Quikk API
Quikk API Setup
Overview
This guide covers how to configure Quikk payments for this app, including credentials, API base URL, webhook callback, request signing, and verification.
Reference API docs:
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, Quikk integration is implemented in:
app/services/quikk/client.rb
2. Configure Routes
Ensure these routes exist:
1
2
3
post "payments/callback", to: "webhooks#quikk"
get "checkout/mpesa_status/:id", to: "checkouts#mpesa_status", as: :mpesa_status_checkout
Purpose:
-
POST /payments/callback: receives Quikk payment webhooks. -
GET /checkout/mpesa_status/:id: supports frontend polling while waiting for payment completion.
3. Set Quikk Webhook URL
In Quikk dashboard/settings, set callback URL to:
https://<your-public-domain>/payments/callback
For local development with a tunnel:
https://<your-tunnel-domain>/payments/callback
Example:
https://cab-bool-wmam-furnished.trycloudflare.com/payments/callback
4. Send Charge Request
Typical request payload shape:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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"
}
}
}
Recommended:
- Keep
data.idandreferencetied to your internal order identifier (for example:ORDER-<order-id>). - Store provider request/charge IDs on your order (
quikk_request_id) for webhook reconciliation.
5. Configure HMAC Authentication
Required request headers:
Content-Type: application/vnd.api+jsonAccept: application/vnd.api+jsonDate: <HTTP GMT date>X-Custom: customAuthorization: keyId="...",algorithm="hmac-sha256",headers="date x-custom",signature="..."
Example:
1
2
3
4
5
6
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Date: Mon, 25 May 2026 09:15:42 GMT
X-Custom: custom
Authorization: keyId="YOUR_API_KEY",algorithm="hmac-sha256",headers="date x-custom",signature="QmFzZTY0U2lnbmF0dXJlJTNE"
Signing string (must match header values exactly and is case-sensitive):
1
2
3
date: <Date header value>
x-custom: custom
Signature process:
- HMAC-SHA256 sign using
quikk.api_secret. - Base64 encode the raw digest.
- URL-encode the base64 result before adding it to
Authorization.
Ruby reference:
1
2
3
4
5
6
7
8
9
10
11
12
13
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}\""
# Remember to append 'Content-Type' and 'Accept' as 'application/vnd.api+json' when making requests
Important:
- ⚠️ Server Clock Drift: The
Datevalue in the header must be the exact value used insigning_string. Ensure your host server’s clock is synchronized via NTP. If your system time drifts by more than a few minutes, Quikk will reject requests with an authentication error. - Header list in
Authorizationmust remainheaders="date x-custom". - Do not send raw base64 signature without URL-encoding.
- Use GMT HTTP date format (
Time.now.httpdatein Ruby).
HMAC Header Generation Scripts
Use one of these scripts to generate auth headers for manual API tests.
Bash (openssl + base64 + jq):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/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 (standalone script):
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
#!/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}"
Quick curl usage pattern:
1
2
3
4
5
6
7
8
curl -X POST "https://tryapi.quikk.dev/v1/mpesa/charge" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-H "Date: ${DATE_HEADER}" \
-H "X-Custom: custom" \
-H "Authorization: ${AUTH_HEADER}" \
--data '{"data":{"type":"charge","id":"ORDER-123","attributes":{"amount":1,"customer_type":"msisdn","customer_no":"2547XXXXXXXX","short_code":"174379","reference":"ORDER-123","posted_at":"2026-05-25T09:15:42.000000Z"}}}'
6. Handle Webhook Variants Safely
Quikk callbacks may provide different ID fields depending on flow. Parse all when present:
data.idattributes.txn_charge_idattributes.resource_idattributes.response_id
Order lookup strategy:
- First match known callback IDs against stored
quikk_request_id. - If
data.idstarts withORDER-, extract order id and match internal order.
Phone field fallback:
- Use
attributes.customer_no || attributes.sender_no.
Status mapping:
- Success:
SUCCESS|SUCCESSFUL|COMPLETED|PAID - Failure:
FAILED|FAIL|ERROR|DECLINED|CANCELLED|CANCELED - Fallback: if
txn_statusis missing buttxn_idexists, treat as success.
Webhook controller location:
app/controllers/webhooks_controller.rb
7. Frontend Polling Contract
Polling response example:
1
2
{ "status": "completed", "payment_status": "paid" }
Expected UI behavior:
- If
payment_status == "paid"(orstatus == "completed"): redirect to confirmation page. - If
status == "failed": redirect to retry page. - Otherwise: continue polling for a bounded time.
Implementation:
-
app/controllers/checkouts_controller.rb(mpesa_status) app/javascript/controllers/mpesa_polling_controller.js
8. Setup Checklist
- Add Quikk credentials for current environment.
- Confirm API base URL (sandbox vs production).
- Ensure callback and polling routes are present.
- Set callback URL in Quikk dashboard.
- Trigger a sandbox payment request.
- Verify webhook reaches
POST /payments/callback. - Confirm order transitions to paid/failed correctly.
- Verify polling flow redirects user from waiting screen.
- Test idempotency by replaying callback payload.
9. Troubleshooting
If payment succeeds on M-Pesa but checkout stays on waiting screen:
- Check webhook logs for exceptions in
WebhooksController#quikk. - Verify callback IDs map to either
quikk_request_idorORDER-<id>. - Ensure status mapping handles your callback payload variant.
- Confirm polling endpoint returns updated status after callback processing.
Enjoy Reading This Article?
Here are some more articles you might like to read next: