Each order needs an ID
Every order must carry a unique identifier in your system. This identifier is what Flot references throughout the lifecycle.
A complete instruction set for merchants to integrate the Flot payment gateway, from KYC and key generation to payment links and webhooks.
Before integrating with Flot, your application must satisfy two requirements: every order needs a stable identifier, and you must expose a webhook endpoint that Flot can notify when an order is processed.
Every order must carry a unique identifier in your system. This identifier is what Flot references throughout the lifecycle.
Implement an endpoint that receives webhook notifications so order status can be tracked once Flot finishes processing.
POST method.Because Flot does not retry webhooks, treat the endpoint as a critical path. Queue the request immediately on receipt and acknowledge with a 2xx response before any heavy processing.
An endpoint behind a load balancer with redundancy, request logging, and idempotent handling of repeated orderId + flotRequestId combinations. Treat the webhook as the source of truth that flips your order from pending to a terminal state.
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -pubout -in private_key.pem -out public_key.pem
cat public_key.pem | base64
You'll reference it on every payment-link request, alongside the signature generated with your private key.
Payment links are generated by making a request to the payment-link generation endpoint.
https://api.app.flotme.ai/merchants/private/v1/payment-linksOnce a payment link is created, the submitted orderId and the internally created order ID (payment attempt ID) can be used to track the payment attempt via:
/merchants/private/v1/external-orders/{externalOrderId}/payment-attempts/{internalOrderId}The payment-link endpoint requires the X-Flot-Merchant-Signature header. Compute its value from the request-related string using the following pipeline:
The request-related string is the stringified request body when a body is present; otherwise it is the canonical request string {METHOD}\n{PATH}. Body-less requests must also include the X-Flot-Merchant-Id header.
X-Flot-Merchant-Signature: <base64( RSA-4096-PSS( SHA-512( requestString ) ) )>Reference implementations in Node.js and Go. Use the same private key whose public counterpart you submitted during onboarding. Any mismatch causes Flot to reject the request. Replace your-merchant-id with the Merchant ID issued by Flot Staff.
Sign the stringified JSON body.
const crypto = require("crypto");
const fs = require("fs");
const privateKey = fs.readFileSync("private_key.pem", "utf8");
const requestBody = {
merchantId: "your-merchant-id",
type: "in-app",
payload: {
orderId: "some-test-order-id-12345",
currency: "SLE",
amount: "10",
},
};
const signer = crypto.createSign("RSA-SHA512");
signer.update(JSON.stringify(requestBody));
const signature = signer.sign(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
},
"base64"
);
payload, _ := json.Marshal(requestBody)
// struct fields: merchantId, type,
// payload{ orderId, currency, amount }
hashed := sha512.Sum512(payload)
signature, err := rsa.SignPSS(
rand.Reader, privateKey, crypto.SHA512,
hashed[:], &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: crypto.SHA512,
})
if err != nil {
return "", err
}
sig := base64.StdEncoding.EncodeToString(signature)
return sig, nil
Sign the canonical {METHOD}\n{PATH} string.
const crypto = require("crypto");
const fs = require("fs");
const privateKey = fs.readFileSync("private_key.pem", "utf8");
const canonical =
"GET\n/merchants/private/v1/external-orders/order-123/payment-attempts/att-456";
const signer = crypto.createSign("RSA-SHA512");
signer.update(canonical);
const signature = signer.sign(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
},
"base64"
);
// Send with the X-Flot-Merchant-Id and
// X-Flot-Merchant-Signature headers
func canonicalRequestString(method, path string) string {
return fmt.Sprintf("%s\n%s", method, path)
}
method := "GET"
path := "/merchants/private/v1/external-orders/order-123/payment-attempts/att-456"
dataToSign := canonicalRequestString(method, path)
sig, err := signPayload(privateKey, []byte(dataToSign))
// Remember the X-Flot-Merchant-Id header
Because there is no body to carry the merchant context, the signature is computed over the canonical {METHOD}\n{PATH} string and the merchant is identified by this header instead.
Basic authentication should be used on the merchant side. Flot includes the credentials you provided at onboarding in every webhook request.
{
"orderId": "order-123",
"flotRequestId": "123456",
"status": "completed"
}
| Field | Type | Description |
|---|---|---|
orderId | string | Order ID in the merchant's system. |
flotRequestId | string | Unique transfer request ID in Flot. |
status | enum | One of completed or failed. |
If a webhook arrives with status = failed, the end-user experienced a payment error while paying with credit card. They can retry later. When your backend receives this status, leave the order in pending state rather than marking it failed.
As of now, Flot Backend sends a webhook notification only once. Design your endpoint for high availability and acknowledge requests promptly with a 2xx response, queuing any heavy work asynchronously.