API Authentication and Credentials

Learn how mCard APIs authenticate requests using HMAC-SHA256 signing, including header format, signature construction, and code examples.

API Authentication and Credentials

All mCard APIs authenticate requests using HMAC-SHA256 request signing. Every API call must include a valid Authorization header containing your API key and a signature computed from the request body.


Credentials

When you are onboarded as a development partner, mCards provides two credentials:

CredentialPurpose
API Key (hmac_key)Identifies your partner account. Included in the Authorization header.
API Secret (hmac_secret)Used to compute the HMAC-SHA256 signature. Never transmitted in requests.

You receive separate credentials for sandbox (UAT) and production environments. Keep your API secret confidential — it should only exist on your server, never in client-side code or version control.


Authorization Header Format

Every request must include an Authorization header in the following format:

Authorization: HMAC_256 {api_key};{signature}

Where:

  • {api_key} is your API key (the hmac_key provided during onboarding)
  • {signature} is the HMAC-SHA256 hex digest of the request body, computed using your API secret

The key and signature are separated by a semicolon (;) with no spaces.


Computing the Signature

The signature is computed by taking the HMAC-SHA256 hex digest of the raw request body using your API secret as the key.

Algorithm

signature = HMAC-SHA256(secret, request_body)

Where:

  • secret = Your API secret (hmac_secret)
  • request_body = The exact raw body string sent over the wire, UTF-8 encoded (the empty string when there is no body)
  • The output is a lowercase hexadecimal string

Query-string parameters, the HTTP method, the path, and headers are not part of the signature — only the body is signed.

For requests with a body (POST, PUT, PATCH)

Sign the exact raw JSON string you send. Serialize the body once, sign that string, and transmit that same string — re-serializing after signing (different key order, added whitespace, pretty-printing) produces a different signature and fails verification.

For requests without a body (GET, DELETE)

Sign the empty string ("").

Do not sign the literal null — signing the four-character string null is the most common cause of 401 invalid_hmac on GET and DELETE requests.

Worked example

Use these fixed values to verify your signing code offline, with the secret test_secret:

RequestSigned stringExpected signature
GET/DELETE (no body)`` (empty)f7f9bd47fb987337b5796fdc1fdb9ba221d0d5396814bfcaf9521f43fd8927fd
POST with body {"foo":"bar"}{"foo":"bar"}7b38395971c202e8b23b8ae5aa82bb18b3d2bf1df564a37257d5185f83f75a1e

Code Examples

Python

import hmac
import hashlib
import json
import requests

API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
BASE_URL = "https://uat-us.mcards.com"  # or https://app-us.mcards.com for production

def make_request(method, path, body=None):
    """Make an authenticated request to the mCard API."""
    # Requests without a body are signed over the empty string
    payload_string = json.dumps(body, separators=(",", ":")) if body else ""
    
    signature = hmac.new(
        API_SECRET.encode("utf-8"),
        payload_string.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()
    
    headers = {
        "Authorization": f"HMAC_256 {API_KEY};{signature}",
        "Content-Type": "application/json"
    }
    
    response = requests.request(
        method,
        f"{BASE_URL}{path}",
        headers=headers,
        data=payload_string if body else None
    )
    return response.json()

# Example: List distributors
distributors = make_request("GET", "/api/distributor/v1/distributors")

# Example: Create a webhook subscription
webhook = make_request("POST", "/api/webhook/webhooks", {
    "url": "https://your-server.com/webhooks",
    "event_type": "transaction"
})

Ruby

require "openssl"
require "json"
require "net/http"

API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
BASE_URL = "https://uat-us.mcards.com"  # or https://app-us.mcards.com for production

def make_request(method, path, body = nil)
  # Requests without a body are signed over the empty string
  payload_string = body ? body.to_json : ""

  signature = OpenSSL::HMAC.hexdigest(
    "sha256",
    API_SECRET,
    payload_string
  )

  uri = URI("#{BASE_URL}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = case method
            when :get    then Net::HTTP::Get.new(uri)
            when :post   then Net::HTTP::Post.new(uri)
            when :put    then Net::HTTP::Put.new(uri)
            when :delete then Net::HTTP::Delete.new(uri)
            end

  request["Authorization"] = "HMAC_256 #{API_KEY};#{signature}"
  request["Content-Type"] = "application/json"
  request.body = payload_string if body

  response = http.request(request)
  JSON.parse(response.body)
end

# Example: List distributors
distributors = make_request(:get, "/api/distributor/v1/distributors")

JavaScript (Node.js)

const crypto = require("crypto");
const https = require("https");

const API_KEY = "your_api_key";
const API_SECRET = "your_api_secret";
const BASE_URL = "https://uat-us.mcards.com"; // or https://app-us.mcards.com for production

function makeRequest(method, path, body = null) {
  // Requests without a body are signed over the empty string
  const payloadString = body ? JSON.stringify(body) : "";

  const signature = crypto
    .createHmac("sha256", API_SECRET)
    .update(payloadString)
    .digest("hex");

  const url = new URL(path, BASE_URL);

  return fetch(url, {
    method: method,
    headers: {
      Authorization: `HMAC_256 ${API_KEY};${signature}`,
      "Content-Type": "application/json",
    },
    body: method !== "GET" ? payloadString : undefined,
  }).then((res) => res.json());
}

// Example: List distributors
makeRequest("GET", "/api/distributor/v1/distributors")
  .then((data) => console.log(data));

cURL

# Set your credentials
API_KEY="your_api_key"
API_SECRET="your_api_secret"

# For GET and DELETE requests, sign the empty string
PAYLOAD=""
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')

curl -X GET "https://uat-us.mcards.com/api/distributor/v1/distributors" \
  -H "Authorization: HMAC_256 ${API_KEY};${SIGNATURE}" \
  -H "Content-Type: application/json"

# For POST requests (signing the JSON body)
PAYLOAD='{"url":"https://your-server.com/webhooks","event_type":"transaction"}'
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')

curl -X POST "https://uat-us.mcards.com/api/webhook/webhooks" \
  -H "Authorization: HMAC_256 ${API_KEY};${SIGNATURE}" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD"

Postman Pre-request Script

If you use Postman for testing, add this as a Pre-request Script to automatically sign requests:

// Set these as Postman environment variables:
// - mcards_api_key
// - mcards_api_secret

const apiKey = pm.environment.get("mcards_api_key");
const apiSecret = pm.environment.get("mcards_api_secret");

// Bodyless requests (GET, DELETE) are signed over the empty string
const body = (pm.request.body && pm.request.body.raw) || "";

const signature = CryptoJS.HmacSHA256(body, apiSecret).toString(CryptoJS.enc.Hex);

pm.request.headers.add({
  key: "Authorization",
  value: `HMAC_256 ${apiKey};${signature}`
});

Environments

mCards provides two environments:

EnvironmentBase URLPurpose
Sandbox (UAT)https://uat-us.mcards.comDevelopment and testing
Productionhttps://app-us.mcards.comLive transactions

API routes are appended to the base URL. For example, to list distributors in sandbox:

GET https://uat-us.mcards.com/api/distributor/v1/distributors
  • Use sandbox for all development and integration testing
  • Each environment has its own set of API credentials
  • API behavior is identical between environments; only the data and base URL differ

Troubleshooting

Common Authentication Errors

ErrorCauseFix
401 Unauthorized — "Invalid Developer Partner"API key not recognizedVerify you are using the correct hmac_key for the target environment
401 Unauthorized — "Inactive Developer Partner"Partner account is disabledContact your mCards partner manager
403 ForbiddenValid credentials but insufficient permissionsVerify your role grants access to the requested API library (see API Scoping and Permissions)

Signature Debugging Checklist

If you receive authentication errors, verify:

  1. Correct secret — You are using the hmac_secret (not the hmac_key) to compute the signature
  2. Correct payload — The signed string exactly matches the raw request body sent over the wire
  3. Correct environment — Sandbox credentials do not work in production and vice versa
  4. JSON serialization — The body is serialized consistently (key order, whitespace) between signing and sending
  5. Encoding — Both the secret and payload are treated as UTF-8 strings
  6. Bodyless requests — GET and DELETE requests are signed over the empty string, not over null. Query parameters are never signed.

Token Validation (JWKS)

When your application receives a Feature SSO Token or Payments User Token from mCards (during embedded partner onboarding flows), you must validate the token's signature before trusting its claims.

JWKS Endpoint

mCards publishes its public keys in JSON Web Key Set (JWKS) format:

Token TypeEnvironmentJWKS URL
Feature SSO TokenProductionhttps://app-us.mcards.com/api/features_marketplace/.well-known/jwks.json
Feature SSO TokenSandbox (UAT)https://uat-us.mcards.com/api/features_marketplace/.well-known/jwks.json
Payments User TokenProductionhttps://app-us.mcards.com/api/payments/.well-known/jwks.json
Payments User TokenSandbox (UAT)https://uat-us.mcards.com/api/payments/.well-known/jwks.json

Production and sandbox publish different key sets — a sandbox token never validates against production keys.

Discovery Endpoint (optional)

If your JWT library prefers OpenID Connect discovery over a hardcoded JWKS URL, Feature SSO also exposes:

GET https://app-us.mcards.com/api/features_marketplace/.well-known/openid-configuration
GET https://uat-us.mcards.com/api/features_marketplace/.well-known/openid-configuration

The response contains a jwks_uri field — use it as-is. It may name the marketplace host alias
(fm.mcards.com in production, fm-uat.mcards.com in sandbox) rather than the host you requested;
both serve the same key set, so either URL is valid.

Validation Steps

  1. Fetch the JWKS — Retrieve the public key set from the appropriate JWKS endpoint. Cache the response (a 1–2 hour TTL is reasonable) and refetch immediately when a token presents a kid you do not have cached — that handles key rotation without any change on your side.
  2. Decode the JWT header — Extract the kid (key ID) from the token's header to identify which key to use.
  3. Verify the signature — Use the matching public key to verify the token's RSA signature (RS256 algorithm).
  4. Check standard claims — Verify exp (expiration), iss (issuer), and aud (audience) claims match expected values. Both token types carry the Features Marketplace base URL for the issuing environment as iss (https://fm.mcards.com/api/features_marketplace/ in production, https://fm-uat.mcards.com/api/features_marketplace/ in sandbox — trailing slash included), which is not the same host as the JWKS URL above.
  5. Extract identity claims — Use the validated claims (consumer_id, cardholder_card, etc.) to identify the cardholder.

Example: Token Validation in Python

import jwt
import requests

# Sandbox (UAT); use https://app-us.mcards.com/... and https://fm.mcards.com/... in production
JWKS_URL = "https://uat-us.mcards.com/api/payments/.well-known/jwks.json"
ISSUER = "https://fm-uat.mcards.com/api/features_marketplace/"

def validate_mcards_token(token):
    """Validate an mCards JWT and return its claims."""
    # Fetch JWKS (cache this in production)
    jwks = requests.get(JWKS_URL).json()
    
    # Decode and verify the token
    header = jwt.get_unverified_header(token)
    
    # Find the matching key by kid
    key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
    public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
    
    claims = jwt.decode(
        token,
        public_key,
        algorithms=["RS256"],
        audience="expected_audience",
        issuer=ISSUER
    )
    return claims

Note: Feature SSO Tokens expire after 5 minutes and are intended for single-use during onboarding. Do not cache or reuse these tokens. See Feature SSO Token and Payments User Token for token-specific claim details.


Webhook Security vs. API Authentication

API authentication (this page) and webhook security are separate mechanisms:

API AuthenticationWebhook Security
DirectionYour server → mCardsmCards → Your server
MechanismHMAC-SHA256 of request bodySignature verification of webhook payload
CredentialsYour API key + secretWebhook-specific signing secret
PurposeProve your identity to mCardsVerify webhook came from mCards

For webhook security details, see Webhook API Concepts.


Did this page help you?