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:

Handaki API — SwaggerHub

1. Configure credentials

Set these credentials per environment:

  • quikk.api_key
  • quikk.api_secret
  • quikk.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:

  1. Creating an HMAC-SHA256 digest using quikk.api_secret.
  2. Base64-encoding the digest.
  3. URL-encoding the Base64 value.
  4. Adding it to the Authorization header.

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.id
  • attributes.txn_charge_id
  • attributes.resource_id
  • attributes.response_id

A reasonable lookup strategy is:

  1. Match known callback IDs against the stored quikk_request_id.
  2. If data.id starts with ORDER-, 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.
  • Date header and signing string match exactly.
  • x-custom is 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.

Official Quikk STK Push setup guide




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
  • Displaying External Posts on Your al-folio Blog
  • Scraping Historical X (Twitter) Posts via Firefox Console
  • Android Package Conflict
  • How to Set Up Cloudflare Tunnel for Local Development