Quikk API Setup
Quikk API Setup
A note before you start
I originally put this guide together after integrating Quikk into a Rails application. We tried taking the integration to production and, honestly, it gave me and the team about two weeks of absolute horror.
The documentation and API behaviour weren’t always as straightforward as we expected, particularly when moving from testing to a real M-Pesa setup.
Quikk actually has a guide that describes the proper setup:
How to Setup Mpesa STK Push (Charge API) on Quikk API
Read that first. It explains the Quikk-side configuration that needs to happen, including setting up the M-Pesa APIs, credentials, shortcode, passkey, and callbacks. The catch is that some of this setup requires contacting Quikk directly, which can be a bit of a hustle when you’re trying to get a production integration working.
This guide focuses more on the application-side integration and the things I needed to account for in the actual Rails application.
Disclaimer: This is based on my experience integrating Quikk into one application and is not a replacement for Quikk’s official documentation or support. API configuration and requirements may change.
Overview
This guide covers:
- Credentials and API configuration
- Webhook callbacks
- M-Pesa charge requests
- HMAC authentication
- Webhook handling
- Frontend payment polling
- Common troubleshooting issues
Reference API documentation:
1. Configure credentials
Set these credentials per environment:
quikk.api_keyquikk.api_secretquikk.shortcode
Expected endpoints:
- Sandbox/Test:
https://tryapi.quikk.dev/v1 - Production:
https://api.quikk.dev/v1
In this codebase, the Quikk integration is implemented in:
1
app/services/quikk/client.rb
Important: Getting the API key and secret is only part of the setup. Your M-Pesa account and the relevant Quikk APIs also need to be configured on the Quikk side.
2. Configure routes
Ensure these routes exist:
1
2
post "payments/callback", to: "webhooks#quikk"
get "checkout/mpesa_status/:id", to: "checkouts#mpesa_status", as: :mpesa_status_checkout
The routes are used for:
-
POST /payments/callback— receiving Quikk payment webhooks. -
GET /checkout/mpesa_status/:id— allowing the frontend to poll for payment completion.
3. Configure the Quikk webhook
Set the callback URL in Quikk to:
1
https://<your-public-domain>/payments/callback
For local development with a tunnel:
1
https://<your-tunnel-domain>/payments/callback
For example:
1
https://cab-bool-wmam-furnished.trycloudflare.com/payments/callback
Make sure the URL is publicly accessible.
4. Send the charge request
A typical charge request looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"data": {
"type": "charge",
"id": "ORDER-<order-id>",
"attributes": {
"amount": 1,
"customer_type": "msisdn",
"customer_no": "2547XXXXXXXX",
"short_code": "174379",
"reference": "ORDER-<order-id>",
"posted_at": "2026-05-25T09:15:42.000000Z"
}
}
}
Keep data.id and reference tied to your internal order identifier.
For example:
1
ORDER-123
It’s also useful to store the provider request/charge ID on your order so that incoming webhooks can be reconciled reliably.
5. Configure HMAC authentication
Required headers include:
1
2
3
4
5
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Date: <HTTP GMT date>
X-Custom: custom
Authorization: keyId="...",algorithm="hmac-sha256",headers="date x-custom",signature="..."
The signing string must match the header values exactly:
1
2
date: <Date header value>
x-custom: custom
The signature is generated by:
- Creating an HMAC-SHA256 digest using
quikk.api_secret. - Base64-encoding the digest.
- URL-encoding the Base64 value.
- Adding it to the
Authorizationheader.
Ruby example:
1
2
3
4
5
6
7
8
9
10
11
12
request_date = Time.now.httpdate
x_custom = "custom"
signing_string = "date: #{request_date}\nx-custom: #{x_custom}"
raw_signature = Base64.strict_encode64(
OpenSSL::HMAC.digest("SHA256", quikk_api_secret, signing_string)
)
encoded_signature = URI.encode_www_form_component(raw_signature)
authorization = "keyId=\"#{quikk_api_key}\",algorithm=\"hmac-sha256\",headers=\"date x-custom\",signature=\"#{encoded_signature}\""
Things that can bite you
Server clock
The Date header must be exactly the same value used in the signing string. Make sure the server clock is synchronized with NTP.
Header order
Keep:
1
headers="date x-custom"
Signature encoding
Don’t send the raw Base64 signature. URL-encode it first.
HTTP date format
Use GMT HTTP date format:
1
Time.now.httpdate
HMAC header generation
For manual API testing, I used the following scripts.
Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail
API_KEY="${QUIKK_API_KEY:?set QUIKK_API_KEY}"
API_SECRET="${QUIKK_API_SECRET:?set QUIKK_API_SECRET}"
X_CUSTOM="custom"
DATE_HEADER="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')"
SIGNING_STRING="date: ${DATE_HEADER}"$'\n'"x-custom: ${X_CUSTOM}"
RAW_SIGNATURE="$(printf '%s' "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)"
ENCODED_SIGNATURE="$(printf '%s' "$RAW_SIGNATURE" | jq -sRr @uri)"
AUTH_HEADER="keyId=\"${API_KEY}\",algorithm=\"hmac-sha256\",headers=\"date x-custom\",signature=\"${ENCODED_SIGNATURE}\""
echo "Content-Type: application/vnd.api+json"
echo "Accept: application/vnd.api+json"
echo "Date: ${DATE_HEADER}"
echo "X-Custom: ${X_CUSTOM}"
echo "Authorization: ${AUTH_HEADER}"
Ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env ruby
require "openssl"
require "base64"
require "uri"
require "time"
api_key = ENV.fetch("QUIKK_API_KEY")
api_secret = ENV.fetch("QUIKK_API_SECRET")
x_custom = "custom"
date_header = Time.now.httpdate
signing_string = "date: #{date_header}\nx-custom: #{x_custom}"
raw_signature = Base64.strict_encode64(
OpenSSL::HMAC.digest("SHA256", api_secret, signing_string)
)
encoded_signature = URI.encode_www_form_component(raw_signature)
authorization = "keyId=\"#{api_key}\",algorithm=\"hmac-sha256\",headers=\"date x-custom\",signature=\"#{encoded_signature}\""
puts "Content-Type: application/vnd.api+json"
puts "Accept: application/vnd.api+json"
puts "Date: #{date_header}"
puts "X-Custom: #{x_custom}"
puts "Authorization: #{authorization}"
6. Handle webhook variants
Quikk callbacks can contain different ID fields depending on the flow. When processing callbacks, account for the fields that may be present:
data.idattributes.txn_charge_idattributes.resource_idattributes.response_id
A reasonable lookup strategy is:
- Match known callback IDs against the stored
quikk_request_id. - If
data.idstarts withORDER-, extract the order ID and match it against the internal order.
For phone numbers, use:
1
attributes.customer_no || attributes.sender_no
For status handling:
1
2
Success: SUCCESS | SUCCESSFUL | COMPLETED | PAID
Failure: FAILED | FAIL | ERROR | DECLINED | CANCELLED | CANCELED
If txn_status is missing but txn_id exists, the application can treat the transaction as successful if that matches the behaviour you’ve verified for your integration.
Webhook controller:
1
app/controllers/webhooks_controller.rb
7. Frontend polling
The frontend can poll the payment status endpoint while waiting for the webhook.
Example response:
1
2
3
4
{
"status": "completed",
"payment_status": "paid"
}
The UI can then:
- Redirect to confirmation when payment succeeds.
- Redirect to retry when payment fails.
- Continue polling while the payment is still pending.
- Stop after a reasonable timeout.
Implementation:
1
2
app/controllers/checkouts_controller.rb
app/javascript/controllers/mpesa_polling_controller.js
8. Setup checklist
- Create/configure the Quikk application.
- Configure the required M-Pesa APIs with Quikk.
- Obtain API credentials.
- Add Quikk credentials to the application.
- Confirm the sandbox/production API URL.
- Configure the callback route.
- Give Quikk the callback URL.
- Send a sandbox payment request.
- Verify the webhook reaches the application.
- Confirm orders transition correctly.
- Test frontend payment polling.
- Replay webhook payloads to test idempotency.
- Test the complete flow in production before relying on it for real orders.
9. Troubleshooting
M-Pesa payment succeeds but checkout stays on the waiting screen
Check:
- Webhook logs for exceptions in
WebhooksController#quikk. - Whether the callback ID matches
quikk_request_id. - Whether the
ORDER-<id>fallback works. - Whether the webhook status is covered by your status mapping.
- Whether the polling endpoint returns the updated order status.
Authentication errors
Check:
- Server time synchronization.
-
Dateheader and signing string match exactly. -
x-customis included in both. -
headers="date x-custom"is correct. - The signature is URL-encoded.
- The correct API key/secret is being used for the environment.
Production setup problems
If everything works in sandbox but production is giving you trouble, check the Quikk-side configuration before spending days debugging your application.
The official Quikk setup guide specifically requires the relevant M-Pesa APIs and account details to be configured on their platform, including credentials, shortcode, passkey, and callback configuration.
That was the part that cost us the most time during our production integration.
Read the official setup guide and contact Quikk directly when required before assuming the API request itself is the problem.
Enjoy Reading This Article?
Here are some more articles you might like to read next: