API Overview

API Overview

QoinPay Enterprise exposes a REST API under /api/v1 for integrating payroll and payments with your surrounding HR, finance, and identity systems. The API is served by the same application you deploy, so it lives entirely within your network and inherits your TLS and access controls.

Your systems call the QoinPay REST API and receive signed webhooks in return.
Your systems call the QoinPay REST API and receive signed webhooks in return.

Base URL and conventions

All endpoints are versioned under /api/v1 on your own host:

https://qoinpay.example.com/api/v1

Requests and responses are JSON. Timestamps are RFC 3339 UTC. Monetary amounts are integer minor units (cents) paired with an ISO 4217 currency code, never floating point. The API version in the path changes only for breaking changes; additive changes are made in place.

Authentication and HMAC request signing

Each integration uses an API key consisting of a public key ID and a secret, issued under Settings → API Keys. The secret is shown once at creation — store it in your secrets manager.

Authentication is not a bearer token. Every request is signed with HMAC-SHA256 over a canonical string, so a captured request cannot be replayed against a different body or after its timestamp window. Build the signature over the method, path, timestamp, and a SHA-256 of the body:

signing_string = METHOD + "\n" + PATH + "\n" + X-QP-Timestamp + "\n" + hex(sha256(body))
signature      = hex(hmac_sha256(secret, signing_string))

Send the key ID, timestamp, and signature as headers. The server recomputes the signature and rejects requests whose timestamp is more than 300 seconds from server time, which bounds replay.

KEY_ID="qp_live_a1b2c3"
SECRET="your-api-secret"
TS=$(date -u +%s)
BODY='{"amount":150000,"currency":"USD","employee_id":"emp_4821"}'
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
SIGSTR=$(printf 'POST\n/api/v1/payouts\n%s\n%s' "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$SIGSTR" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

curl -sS https://qoinpay.example.com/api/v1/payouts \
  -H "X-QP-Key-Id: $KEY_ID" \
  -H "X-QP-Timestamp: $TS" \
  -H "X-QP-Signature: $SIG" \
  -H "Idempotency-Key: 3f9c1e7a-payout-2026-08" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Idempotency

Any request that creates or moves money must carry an Idempotency-Key header — a client-generated unique value (a UUID is ideal). The server stores the first response against that key for 24 hours and returns the same response, without repeating the side effect, for any retry using the same key. This makes retries after a network timeout safe: you either created the payout once, or you did not, but never twice. Reusing an idempotency key with a different body is rejected as a conflict, which catches accidental key collisions.

Cursor pagination

List endpoints use cursor pagination, not offset pages, so results stay stable while data changes underneath you. A list response includes a next_cursor when more records exist:

{
  "data": [ { "id": "payout_881" }, { "id": "payout_882" } ],
  "next_cursor": "eyJpZCI6InBheW91dF84ODIifQ",
  "has_more": true
}

Pass it back as the cursor query parameter to fetch the next page, with an optional limit (default 50, maximum 200):

GET /api/v1/payouts?cursor=eyJpZCI6InBheW91dF84ODIifQ&limit=100

When has_more is false and next_cursor is absent, you have reached the end. Never construct cursors yourself — treat them as opaque.

Signed webhooks

Rather than polling, subscribe to webhooks for asynchronous events such as payout.settled, payroll_run.completed, and license.seat_released. QoinPay POSTs a JSON event to your endpoint and signs each delivery so you can verify it originated from your installation.

Each delivery carries an X-QP-Signature header — an HMAC-SHA256 of the raw request body computed with your webhook signing secret. Verify it against the raw bytes before parsing, and reject any mismatch:

$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expected, $request->header('X-QP-Signature'))) {
    http_response_code(400);
    exit;
}

Deliveries include an event id; treat handling idempotently, because at-least-once delivery means an event may arrive more than once. Respond 2xx quickly — acknowledge and process asynchronously. Failed deliveries are retried by the cron worker with exponential backoff.

OpenAPI 3.1

The complete, authoritative contract is published as an OpenAPI 3.1 document served by your installation:

GET /api/v1/openapi.json

Generate client SDKs, load it into Postman or your API gateway, and validate requests against it. Because the spec is served by the running application, it always matches the version you have deployed — there is no drift between documentation and behaviour.

Errors

Errors use standard HTTP status codes with a structured body: 400 for validation, 401 for a bad signature, 403 for insufficient scope, 409 for idempotency conflicts, 422 for semantic rejection, and 429 when rate limited. Each body includes a machine-readable code and a human-readable message. Consult the OpenAPI document for the per-endpoint error catalogue, and see Getting Started for enabling the cron worker that drives webhook delivery.