Reference

Webhooks

The signature-verified payment-gateway webhook: verification & retries.

GrivaOne integrates with an online payment gateway for subscription payments. The gateway notifies GrivaOne of payment outcomes via a single, signature-verified webhook. This is an inbound callback — your application does not call it, and it is the only place a payment is ever marked paid.

Payment-gateway endpoints

GET/payments-gateway/configBearer

Non-secret gateway config: whether it is configured, the publishable key and the webhook path to register.

POST/payments-gateway/checkoutBearer

Start a hosted-checkout payment (OWNER/ADMIN); returns the URL to redirect to. 412 if the gateway is not configured.

GET/payments-gateway/checkout/:idBearer

Poll a checkout’s status after the provider redirects back. Reflects the webhook truth, not the browser.

POST/payments-gateway/webhookPublic

Inbound provider callback — signature-verified. This is the only place a payment is marked paid.

The checkout flow

  1. Check whether the gateway is live for the tenant with GET /payments-gateway/config. It returns a configured flag and the public publishable key — never the secret key or webhook secret.
  2. Start a payment with POST /payments-gateway/checkout and redirect the customer to the hosted-checkout URL it returns. If the gateway is not configured this responds 412 Precondition Failed.
  3. After the provider redirects back, confirm the outcome with GET /payments-gateway/checkout/:id. Its status reflects the verified webhook — not what the browser reports.
curl · GET /payments-gateway/config
curl https://gulfpay-production.up.railway.app/api/v1/payments-gateway/config \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
curl · POST /payments-gateway/checkout
curl -X POST https://gulfpay-production.up.railway.app/api/v1/payments-gateway/checkout \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "organizationId": "org_123", "planId": "pro_monthly" }'

How the webhook is secured

The provider sends a server-to-server POST to /payments-gateway/webhook. Because providers cannot carry a GrivaOne bearer token, the endpoint is public — but every delivery is verified before anything is trusted:

  • The shared secret / signature is checked against the raw request bytes exactly as the provider sent them. An unsigned or forged delivery is rejected with 403 Forbidden.
  • Delivery is idempotent by provider event id, so a retried or duplicated event is a safe no-op.
  • A payment is recorded only on a genuine paid event. A verified event (including a duplicate) returns 200 so the provider stops retrying.

Verifying a signature

Signature verification always runs against the raw request body — the exact bytes the provider sent. Compute the HMAC over those bytes with the shared secret and compare it to the signature header using a constant-time comparison. GrivaOne does this internally; the same pattern applies if you ever receive provider callbacks directly.

javascript · HMAC verification (illustrative)
// Illustrative HMAC verification of a provider callback.
// GrivaOne performs this server-side; the shape mirrors the pattern you would
// use if you receive provider callbacks of your own. Verify against the RAW,
// unparsed request bytes — parsing then re-serialising changes them.
import crypto from 'node:crypto'

function isGenuine(rawBody /* Buffer */, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)          // raw bytes, exactly as received
    .digest('hex')
  // Constant-time compare avoids leaking the secret via timing.
  const a = Buffer.from(expected)
  const b = Buffer.from(signatureHeader || '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Never parse before you verify

If you JSON-parse and re-serialise the body before hashing, the bytes change and every signature check fails. Capture the raw body first, verify, then parse. Reject anything that does not verify with 403.

Retries & idempotency

Providers retry a webhook until they receive a success response, so the same event can arrive more than once. Two rules keep that safe:

  • Acknowledge fast. Return 2xx as soon as the event is verified and accepted; do heavy work afterwards. A slow handler looks like a failure and triggers more retries.
  • Deduplicate by event id.Delivery is idempotent on the provider's event id, so a replayed event is a no-op — the payment is recorded exactly once regardless of how many times it is delivered.

Because the browser redirect is not authoritative, always confirm the outcome server-side with GET /payments-gateway/checkout/:id, which reflects the verified webhook rather than what the browser reported.

No outbound webhooks yet

GrivaOne does not currently deliver outbound event webhooks to your systems (for example, “invoice created” or “payment received” callbacks). To react to changes today, poll the relevant endpoints. Outbound webhooks are on the roadmap.