For developers

Partner Sync API

Push your per-store stock — or your restaurant menu — into Stockwaka, and pull back the orders its WhatsApp storefront generates for you. Built so a retail or restaurant chain can add WhatsApp ordering as an additional channel — without moving its stock ledger or menu, changing its counter or POS, or replacing anything it already runs.
Version
v1
Base URL
https://api.stockwaka.com/api/v1
Format
JSON over HTTPS
Auth
Bearer API key, scoped

1. The one rule

Your system is authoritative for stock. Stockwaka is a mirror.

A retail chain will not hand its stock ledger to a new vendor, and two systems each applying their own changes diverge permanently the first time one of them misses a message. So Stockwaka does not try to be a second ledger.

Concretely, that means three things:

  • A stock sync SETS an absolute on-hand figure. It never applies a change. Re-sending the same payload does nothing; a payload you dropped is corrected by the next one. There is no ordering requirement between pages and no reconciliation protocol to implement.
  • Stockwaka holds only a short reservation window against your figure while a shopper pays — never a parallel count. See Reservations, below.
  • Stockwaka orders are requests you accept, not deductions imposed on you. You take an order in, acknowledge it, and your next sync carries the consequence back to us as an ordinary absolute count.

If you are ever tempted to send us what changed since last time, send the current count instead. The API rejects a negative quantity for exactly this reason.

2. Getting access

There is no self-service signup. A partner integration is set up with a person, because binding your store codes to the right shops is a decision nobody should make by guessing.

  1. 1Get in touch. Email support@bitasei.com with your company, roughly how many stores you run, and which system will be talking to us (SAP, Odoo, Microsoft Dynamics, an in-house ERP — it makes no difference to the API, but it tells us who to put you with).
  2. 2A short technical call. We agree which stores go live first, and which scopes each of your jobs needs.
  3. 3Your account and your stores are set up. One Stockwaka account for the chain, with one shop under it per store — created and named from your own store list, however many that is. If some of your branches already use Stockwaka on WhatsApp, nothing about their accounts changes.
  4. 4We issue your test key. It arrives once, over a secure channel, and starts with swp_test_. Stockwaka stores only a hash of it, so it can never be shown again — a lost key is replaced, not recovered.
  5. 5You build against the sandbox using the reference below. Start with GET /partner/whoami; if that returns your business name, everything else is downhill.
  6. 6Bind your store codes with PUT /partner/stores/:code, once per store. The call is idempotent, so it belongs in your deploy script rather than in a runbook.
  7. 7Pilot on a handful of real stores. We watch the first syncs together — the two numbers that matter early are unpricedCount and skipped, which between them tell you whether your export is mapping cleanly.
  8. 8Go live. We issue the production key (swp_live_) and you bring stores on as fast as you like. Nothing is all-or-nothing: a store whose code you have not bound yet is simply not synced, and binding it later needs no change on our side.

One key. Every store.

A key belongs to your company account, not to a store, and it reaches every shop under it. The storeCode on each request is an address, not a credential — it picks the shelf, while the key proves it is you asking. A chain of two hundred stores needs exactly as many keys as a chain of one.

The only reason to hold more than one key is to separate jobs, not stores: give your stock job stock:read and stock:write, your order collector orders:read and orders:ack. Then a bug in one cannot damage the other’s domain, and a leaked key is partial rather than total.

What your merchant sees

Nothing changes for them. Stockwaka stays what it was — voice notes on WhatsApp, the counter app, the storefront — and the stock they see is now kept in step with yours automatically. They keep full control of prices, and a price they set themselves is never overwritten by a sync that does not carry one.

3. Authentication

Send your key as a bearer token on every request.

Every request

Authorization: Bearer swp_live_...

A key looks like swp_<env>_<43 characters>, where env is live or test. The environment is part of the key so a test credential pasted into a production config is visibly wrong before it is used.

Scopes

Ask for the narrowest set each job needs. Your nightly stock push has no business reading your customers’ phone numbers.

ScopeGrants
stock:readList stores, and read back the stock we hold.
stock:writeBind store codes, and push stock.
orders:readPull the orders your storefront generated.
orders:ackAcknowledge an order into your own system.
orders:writeConfirm, reject, start (preparing), ready or fulfil an order from your own system.
webhooks:manageRegister, test, rotate and remove your order webhook. Registering a URL also needs orders:read.

GET /partner/whoami needs no scope at all — it is how you find out what you hold.

What a key can reach

A key resolves to exactly one account, and that account is read from the key itself.

Nothing you send in a URL, query string or body can change which account you are acting as. A store code belonging to another business does not exist as far as your key is concerned — it returns 404, not someone else’s data.

Failures

StatusMeaning
401Missing, malformed, unknown or revoked key.
403The key is valid but lacks a required scope, or the Stockwaka account is not active.

A 403 never means “rotate your key”. Read the message — it names the scope you are short of.

4. Stores

Stockwaka calls a shop a branch. You call it a store, with your own code (SW-IKJ-01). Binding the two is a one-time setup step per store.

GET/partner/storesstock:read

Lists every shop on the account, including ones you have not bound yet.

Response

[
  { "storeCode": "SW-MAIN",   "branchId": "primary",                  "name": "Lekki",         "isPrimary": true,  "isActive": true },
  { "storeCode": "SW-IKJ-01", "branchId": "66f1a0c2e4b0a1d2c3e4f5b1", "name": "Ikeja",         "isPrimary": false, "isActive": true },
  { "storeCode": null,        "branchId": "66f1a0c2e4b0a1d2c3e4f5b2", "name": "Yaba",          "isPrimary": false, "isActive": true }
]

branchId is opaque. One shop on every account is the main shop, and its branchId is the literal string "primary" rather than an id. That is not a placeholder — it is a real, permanent handle you can use anywhere a branchId is accepted.

PUT/partner/stores/:externalCodestock:write

Binds your store code to one shop. Idempotent — safe to run on every deploy.

Request

# by the id from GET /partner/stores  (recommended for scripts)
curl -X PUT "$BASE/partner/stores/SW-IKJ-01" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"branchId": "66f1a0c2e4b0a1d2c3e4f5b1"}'

# the main shop
curl -X PUT "$BASE/partner/stores/SW-MAIN" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"branchId": "primary"}'

# by name — convenient by hand; refused if it matches more than one shop
curl -X PUT "$BASE/partner/stores/SW-YAB-01" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"branchName": "Yaba"}'

A code already bound to a different shop on the same account returns 409 and names the shop holding it. Re-bind that one first.

5. Stock sync

POST/partner/stock/syncstock:write

The main endpoint. An absolute per-store upsert.

Request

curl -X POST "$BASE/partner/stock/sync" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "storeCode": "SW-IKJ-01",
    "items": [
      { "externalRef": "SKU-88213", "name": "Amlodipine 5mg 30s", "quantity": 42,
        "sellingPrice": 2400, "costPrice": 1800, "barcode": "6154001234567",
        "unit": "pack", "category": "Medicines" },
      { "externalRef": "SKU-88214", "quantity": 0 }
    ]
  }'

Response

{
  "storeCode": "SW-IKJ-01",
  "branch": "Ikeja",
  "total": 2, "created": 0, "updated": 2, "skipped": 0,
  "unpricedCount": 0,
  "skippedSamples": [],
  "warnings": [],
  "photosQueued": 0,
  "reserved": 3,
  "sellable": 39,
  "syncedAt": "2026-08-18T09:00:00.000Z"
}

Item fields

FieldRequiredNotes
externalRefstrongly recommendedYour SKU — the join key. See below.
nameon first sight of a SKUThe product's name as your customers should see it.
quantityno (defaults to 0)Absolute on-hand. Integer, never negative. Ignored for a made_to_order product.
sellingPricenoWhole naira. Omit to leave the merchant's own price alone.
costPricenoWhole naira.
barcodenoEAN/UPC, for scanning at the counter.
unitnopack, bottle, carton…
categorynoBrowsing group shown to shoppers.
stockModenotracked (counted stock) or made_to_order (a dish). See Menus.
soldOutnoMade-to-order only. true takes the dish off sale; false puts it back.
soldOutUntilnoISO 8601. Takes a dish off sale until then. Implies soldOut: true.
modifierGroupsnoA dish's complete option groups, replacing what it had.
descriptionnoShopper-facing copy for the photo menu, kept to 280 characters.
imageUrlnoA public http(s) link to the product photo, fetched in the background.

Any field we do not recognise is ignored, not rejected — adding a column to your export will never break the integration. A field you omit always means “leave what Stockwaka holds alone”; it never means “clear it”.

externalRef is the join key

Send your own stable SKU on every line. Stockwaka matches an incoming row against the catalogue in this order: externalRef, then barcode, then the product name.

Matching on name alone is fragile. The first time you correct a product’s description, a name-matched sync would create a second row and split that product’s stock across both. A SKU cannot do that.

You do not need to migrate an existing shop.

On the first sync we match by barcode or name and adopt your SKU onto the row we found. Every sync after that matches on the SKU and is immune to renames on either side. Once a SKU is known, you may push counts without repeating the name.

Once a SKU is known

{ "storeCode": "SW-IKJ-01", "items": [{ "externalRef": "SKU-88213", "quantity": 17 }] }

Absolute, not delta

  • Sending the same payload twice leaves the quantity unchanged. Retry freely.
  • A lower number is applied as-is. It is a count, not an increment.
  • quantity: 0 is how you say “none left” for counted stock. There is deliberately no delete: a zeroed product keeps its price, barcode and SKU, so restocking it later is one more sync rather than a re-setup. A made-to-order dish has no count — take it off sale with soldOut instead.
  • A negative quantity is rejected for the whole request, with nothing written, because it is evidence the payload is deltas — in which case every other row in it is wrong too.

Response fields worth alerting on

FieldMeaning
unpricedCountProducts now in the catalogue with no selling price. They cannot be sold online. If your export has a price column, map it — this is the field to alert on.
skippedRows we could not use. Never fails the batch; skippedSamples carries up to five human-readable reasons for your logs.
warningsUp to five things we did not apply from rows we otherwise did — a soldOut sent for counted stock, a photo link that is not a link.
photosQueuedProducts whose photo is being fetched in the background. 0 once every photo is current.
reservedUnits at this store currently held against open orders. Counted stock only.
sellableUnits a shopper could order right now (on-hand minus reserved). Counted stock only.

Partial success

Individual bad rows are skipped, counted and reported — they never fail the batch. A contract error rejects the whole request and writes nothing, because it means the payload as a whole cannot be trusted: no storeCode, items not a list, over the size cap, a negative or non-numeric quantity, a stockMode other than tracked or made_to_order, or a soldOut / soldOutUntil that cannot be read. The last two are contract errors on purpose: a mis-mapped availability column would otherwise take every dish on, or off, every menu at once.

7. Stock read-back

GET/partner/stockstock:read

What Stockwaka currently holds, for reconciliation.

Request

curl "$BASE/partner/stock?storeCode=SW-IKJ-01&limit=200" \
  -H "Authorization: Bearer $KEY"

Response

{
  "items": [
    {
      "externalRef": "SKU-88213",
      "name": "Amlodipine 5mg 30s",
      "storeCode": "SW-IKJ-01",
      "branchId": "66f1a0c2e4b0a1d2c3e4f5b1",
      "stockMode": "tracked",
      "quantity": 42, "reserved": 3, "available": 39,
      "soldOut": false, "soldOutUntil": null,
      "sellingPrice": 2400, "costPrice": 1800,
      "barcode": "6154001234567", "unit": "pack", "category": "Medicines",
      "description": null, "imageUrl": null, "modifierGroups": [],
      "updatedAt": "2026-08-18T09:00:00.000Z"
    }
  ],
  "nextCursor": "66f1a0c2e4b0a1d2c3e4f5c9"
}
  • quantity, reserved and available are null for a made-to-order dish — it has no count, and a 0 would read as “none left”.
  • soldOutUntil is null while a dish is on sale, and also while it is off until further notice.
  • modifierGroups come back in the shape you send them, with minSelect resolved (1 for a plain required group).
  • imageUrl is Stockwaka’s stored copy of the photo, never the link you sent.
QueryDefaultNotes
storeCodeall storesOmit to walk the whole estate in one pass.
limit100Max 200. Values above are clamped, not rejected.
cursor—Pass the previous page’s nextCursor.

Page until nextCursor is null. An item present here but absent from your system is usually one the merchant added themselves on WhatsApp — it will have externalRef: null.

8. Reservations, and why your count and ours can differ

This is the one place your count and ours legitimately differ, and the reserved and sellable fields exist to explain it.

When a shopper places an order, Stockwaka reserves the units for a short window — 15 minutes, extended to at most 24 hours once they say they have paid — so two shoppers cannot buy the same last pack. Nothing is deducted: the units are still on your shelf and still in your count.

your on-hand:  42        (what you sent)
reserved:       3        (held by open orders)
sellable:      39        (what a shopper can order right now)

A reservation survives your sync. If you push quantity: 20 while three units are reserved, the result is 20 on hand with 3 still held and 17 sellable. Your figure is authoritative for on-hand; the reservation is ours.

Reservations release automatically when an order is confirmed, cancelled or expires. The only lasting effect on your side is the confirmed sale, which appears as an order in the pull below.

9. Orders

GET/partner/ordersorders:read

A change feed, ordered by when each order last changed. Poll it with the cursor you last received.

Request

# first ever pull
curl "$BASE/partner/orders?limit=100" -H "Authorization: Bearer $KEY"

# every pull after that
curl "$BASE/partner/orders?limit=100&cursor=$CURSOR" -H "Authorization: Bearer $KEY"

Response

{
  "orders": [
    {
      "reference": "7QM4P2",
      "status": "preparing",
      "storeCode": "PZ-IKJ",
      "branchId": "66f1a0c2e4b0a1d2c3e4f5b1",
      "customerName": "Mrs Adebayo",
      "customerPhone": "2348012345678",
      "customerNote": "12 Allen Avenue, opposite GTBank",
      "fulfillmentType": "delivery",
      "deliveryLocation": { "latitude": 6.6018, "longitude": 3.3515 },
      "items": [
        { "externalRef": "PIZ-PEP-L", "name": "Pepperoni Pizza (Large)",
          "quantity": 2, "unit": null, "unitPrice": 12000, "lineTotal": 28000,
          "modifiers": [
            { "group": "Crust", "label": "Stuffed crust", "priceDelta": 1500, "externalRef": "MOD-CR-ST" },
            { "group": "Extra toppings", "label": "Mushroom", "priceDelta": 500, "externalRef": "MOD-TP-MU" }
          ] }
      ],
      "totalAmount": 28000, "taxAmount": 0,
      "deliveryFee": 1500, "deliveryFeeOnArrival": false, "payableAmount": 29500,
      "placedAt": "2026-08-18T08:41:00.000Z",
      "confirmedAt": "2026-08-18T08:43:00.000Z",
      "preparingAt": "2026-08-18T08:44:00.000Z", "readyAt": null,
      "fulfilledAt": null, "cancelledAt": null,
      "createdAt": "2026-08-18T08:39:00.000Z",
      "updatedAt": "2026-08-18T08:44:00.000Z",
      "ackedAt": null, "externalOrderId": null
    }
  ],
  "nextCursor": "2026-08-18T08:44:00.000Z|66f1a0c2e4b0a1d2c3e4f5d3"
}
QueryDefaultNotes
cursor—Opaque. Always prefer this. Wins over since if both are sent.
since—ISO 8601. For a first pull only.
storeCodeall stores
limit50Max 200.

Use the cursor, not `since`.

The cursor is an exact position in a total order; a timestamp is not, and two orders touched in the same millisecond can be missed by timestamp paging. An empty orders array means you are caught up — keep the cursor and reuse it.

Because this is a change feed, an order you have already seen reappears when its status changes. That is the point: you never miss a confirmation or a cancellation.

Order statuses

StatusMeaning
pending_paymentPlaced. Stock reserved, waiting for the shopper to pay. Do not fulfil.
payment_claimedThe shopper says they have paid; the merchant is verifying.
confirmedPaid. Take it in.
preparing / readyPaid and still open — the optional kitchen states. Take it in if this is the first time you see it.
fulfilledHanded over or delivered. Terminal — but paid, so take it in if you have never seen it.
cancelled / expiredDead. The stock reservation was released.

Draft carts are never published. Take in any paid status you have not seen — not only confirmed. A kitchen’s till moves an order to preparing or ready within a minute of payment, so a feed polled every few minutes often meets it there first.

Order fields

  • fulfillmentType is pickup, delivery or dine_in. For a delivery the address is in customerNote; for dine-in, the table.
  • deliveryLocation is the exact point a delivery goes when the shopper shared a location pin, as named latitude and longitude. null for pickup, dine-in and typed addresses.
  • unitPrice is the base price per unit, before options. lineTotal is (unitPrice + Σ modifiers[].priceDelta) × quantity — options are priced per unit.
  • modifiers[].externalRef is the option code you sent on a sync, frozen when the shopper chose it; null for an option the merchant set up on WhatsApp.
  • totalAmount is the goods — what to take in as the sale. deliveryFee sits beside it, never inside it; deliveryFeeOnArrival is true when the rider collects that fee. payableAmount is what the shopper actually paid at checkout. Every money figure is in the shop’s own currency, in whole units.
  • preparingAt and readyAt stamp the kitchen states, for ticket times.
  • items[].note is the shopper’s instruction for that line — “no onions” — and kitchenNote their note for the whole order. Both are null when empty; print them on the ticket. A fixed choice (“Pepper: Normal / Extra”) belongs in modifierGroups with priceDelta: 0, which carries your option code.
  • Line items carry externalRef where we know it. It is null for a product the merchant added themselves on WhatsApp — we will not invent a SKU your system has never issued.

An order you have already taken in can come back with more than a new status: when a payment confirms automatically, the shopper may choose pickup or delivery a moment later. Update the order you hold rather than skipping it.

POST/partner/orders/:reference/ackorders:ack

Records that your system has taken the order in. Idempotent.

Request

curl -X POST "$BASE/partner/orders/7QM4P2/ack" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalOrderId": "ERP-2026-00184"}'

externalOrderId is optional and stored so support can trace an order across both systems. A repeat ack returns alreadyAcked: true and changes nothing — including ackedAt, which stays the moment you first took the order. Acking does not advance an order’s updatedAt, so an acknowledged order will not come back on the next pull just because you acknowledged it.

An ack is not a fulfilment. It means “we have this”. Use the four actions below to move the order itself.

Order actions

POST/partner/orders/:reference/confirm | reject | preparing | ready | fulfilorders:write

Five things your system can do to an order. Each returns 200 and each is idempotent — a repeat returns alreadyDone: true and changes nothing, so a retrying job is always safe.

ActionWhat it does
confirmThe money landed. Records the sale, deducts stock, releases the reservation, tells the shopper.
rejectYou cannot fill it. Releases the reservation and tells the shopper. Optional {"reason": "…"}.
preparingThe kitchen has started it. Optional — for a kitchen display that bumps tickets.
readyCooked, or picked, and waiting. This is the one the shopper is messaged about.
fulfilHanded over. Terminal.

Request

curl -X POST "$BASE/partner/orders/7QM4P2/confirm" \
  -H "Authorization: Bearer $KEY"

Response

{
  "reference": "7QM4P2",
  "status": "confirmed",
  "alreadyDone": false,
  "updatedAt": "2026-08-18T09:05:11.000Z"
}

status is read back after the action, so you can store the row you were given rather than inferring what the action must have produced.

orders:write is a separate scope from orders:ack on purpose.

An ack is a passive receipt. Confirming moves money and deducts stock; rejecting releases a shopper’s hold. An order-collection job should carry orders:read + orders:ack and nothing more.
  • Confirm is the same rung as a merchant confirming by hand, so your system and the shop owner can race safely — whichever lands second reports alreadyDone rather than deducting twice.
  • Reject only works before payment. Once an order is confirmed the money has been taken and the stock deducted, so unwinding it is a refund — a decision this API will not take on a merchant’s behalf. It returns 409.
  • Preparing and ready need a paid order. Both are reachable straight from confirmed, so you never have to post a state you do not model — a kitchen that only knows “ready” never posts preparing. An order already past the state you post returns 409.
  • None of these writes a stock figure. Confirming moves units the shopper had already reserved into a sale; your next absolute sync still overwrites whatever we hold.

You do not have to use them. A merchant can drive every one of these from WhatsApp or the till, and a small shop does. These exist because nobody types CONFIRM 7QM4P2 a hundred times a day, and your back office already knows when it took the money.

10. Webhooks

Instead of polling every 30–60 seconds, register one HTTPS endpoint and every order change is posted to it within seconds — the same order object GET /partner/orders returns.

PUT/partner/webhookwebhooks:manage + orders:read

Request

curl -X PUT "$BASE/partner/webhook" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://erp.example.com/hooks/orders"}'

The response carries a secret (whsec_…) the first time only — store it like your API key. One webhook per account; PUT again to move it (the secret is kept) or to switch a disabled one back on. Registering needs orders:read too, because an endpoint receives order data.

What arrives

A POST signed to the open Standard Webhooks specification, so you can verify it with a published library.

HeaderValue
webhook-idThe same for a retry of the same order state — de-duplicate on it.
webhook-timestampUnix seconds when this attempt was sent.
webhook-signaturev1,<base64 HMAC-SHA256 of "id.timestamp.body">. Two, space-separated, during a secret rotation.

Body

{
  "id": "msg_7QM4P2_1755507911000",
  "type": "order.updated",
  "createdAt": "2026-08-18T09:05:11.000Z",
  "data": { "order": { "reference": "7QM4P2", "status": "confirmed", "…": "…" } }
}
  • Answer any 2xx within 10 seconds, then do the work. Redirects are not followed.
  • The order is sent as it stands when the delivery goes out, so a burst of changes arrives as one delivery of the latest state.
  • Delivery is at least once. Verify the signature over the raw body, and reject a timestamp more than five minutes old.

Retries, rotation and removal

A failed delivery is retried after 30s, 2m, 10m, 30m, 1h, 3h, 6h, 12h, 24h — about two days. An endpoint that answers 410 Gone, or where 20 deliveries in a row fail every retry, is switched off; GET /partner/webhook shows why, and PUT switches it back on.

CallDoes
GET /partner/webhookThe webhook and its health.
POST /partner/webhook/rotate-secretA new secret. The old one keeps signing alongside it for 24 hours.
POST /partner/webhook/testSends a webhook.test event now and returns what your endpoint answered.
DELETE /partner/webhookRemoves it.

A webhook is the fast path, not the record.

Keep polling GET /partner/orders with your cursor every 10–15 minutes. Anything a webhook could not deliver — your endpoint was down, the webhook was switched off — is always there.

11. Errors and limits

Standard HTTP status codes, with a message written to be read by a human.

Response

{
  "statusCode": 400,
  "message": "\"items\" carried 1500 products, which is over the limit of 1000 per request. Split the store's catalogue into pages of 1000 or fewer…",
  "error": "Bad Request"
}
StatusWhen
400The payload breaks a contract rule — no storeCode, over the size cap, a negative quantity, an unreadable stockMode, soldOut or soldOutUntil. Nothing was written.
401Missing, malformed, unknown or revoked key.
403Valid key, missing scope; or the Stockwaka account is not active.
404An unrecognised store code, or an order reference not on this account.
409A store code already bound to another shop, or an ambiguous branch name.

An unrecognised storeCode is a 404 and never falls back to the main shop.

If it did, renaming a store in your system would silently redirect that store’s whole catalogue onto another building’s shelf — and nothing would look wrong until a customer was sold something that is not there.

Retrying

Everything here is safe to retry: the sync sets absolute figures, the ack is idempotent, and reads have no side effects. Retry 5xx and network failures with exponential backoff; do not retry a 4xx without changing the request.

Limits

LimitValue
Items per stock/sync call1,000
limit on GET /partner/stock100 default, 200 max
limit on GET /partner/orders50 default, 200 max
Option groups per product10
Options per group30
Option-group name or option label80 characters
imageUrl length2,048 characters
descriptionkept to 280 characters
Store code length64 characters

A large chain’s catalogue must be paged. Pages are independent — each is applied on its own, order does not matter, and a failed page can be re-sent alone. Send pages sequentially per store: two concurrent syncs of the same store would both be applied, and the last to land wins.

13. Support

Questions, a key rotation, or a new store code: email support@bitasei.com or call +234 907 888 4939.

Never send us your key.

Include your keyPrefix — the visible leading characters, which GET /partner/whoami returns — and the storeCode in question. That is enough for us to find the credential. If a key has been exposed, say so and we will revoke and reissue it the same day.