Authentication

Graph uses API keys to authenticate requests. Send your API key in the Authorization header as a bearer token.

Authorization: Bearer <api_key>

API keys are environment-specific:

  • sk_live_... keys are used for live requests.
  • sk_test_... keys are used for test requests.

Keep API keys secret. Do not expose them in client-side code, mobile apps, public repositories, logs, or support tickets.

API Key Versions

Graph supports two API key types:

TypeDescription
v1Legacy bearer-only API keys. Existing keys continue to work.
v2API keys that support bearer authentication and optional HMAC request signing.

Existing v1 keys continue to work without request signing. To use request signing, create a new v2 API key in your Graph dashboard.

One-Time Secret Visibility

When you create a v2 API key, Graph returns two secrets:

SecretPurpose
api_keyUsed in the Authorization header.
signing_secretUsed to sign requests when request signing is enabled.

These plaintext values are shown only once, when the key is created. After creation, Graph only shows safe metadata such as the key ID, label, environment, signing mode, and the last 4 characters of each secret.

Store both secrets securely in a secret manager.

Request Signing

Request signing lets your server prove that a request was produced by a holder of the signing_secret. It helps protect against request tampering and replay.

Signed requests use two headers:

X-Oval-Timestamp: <unix_timestamp_seconds>
X-Oval-Signature: <hex_hmac_sha256_signature>

X-Oval-Timestamp must be a Unix timestamp in seconds.

X-Oval-Signature must be a lowercase hexadecimal HMAC-SHA256 signature created with your signing_secret.

Signing Modes

Each v2 API key has a signing_mode.

ModeBehavior
offRequest signing is disabled. Only bearer authentication is required.
optionalUnsigned requests are accepted. If signing headers are present, they must be valid.
requiredEvery request must include a valid signature.

We recommend starting with optional while testing your integration, then switching to required once your signing implementation is working.

Creating the Signature

To create a signature, build this canonical string:

{timestamp}.{METHOD}.{requestURI}.{rawBody}

Where:

PartDescription
timestampThe same Unix timestamp used in X-Oval-Timestamp.
METHODThe uppercase HTTP method, such as GET, POST, or DELETE.
requestURIThe request path including the query string, if present.
rawBodyThe exact request body string sent to Graph. For requests without a body, use an empty string.

Then compute:

HMAC_SHA256(signing_secret, canonical_string)

Encode the result as lowercase hexadecimal and send it as X-Oval-Signature.

GET Requests

GET requests usually do not have a body. For GET requests, the rawBody portion is an empty string.

The canonical string still includes the trailing ..

Example:

1760000000.GET./transaction?page=1&per_page=20.

POST Requests

For requests with a JSON body, sign the exact raw JSON string you send.

Example body:

{"amount":1000,"currency":"USD"}

Canonical string:

1760000000.POST./payout.{"amount":1000,"currency":"USD"}

If your JSON serializer changes whitespace or field order, the signature changes too. Always sign the exact request body sent over the wire.

Example: Node.js

import crypto from "crypto";

const apiKey = process.env.GRAPH_API_KEY;
const signingSecret = process.env.GRAPH_SIGNING_SECRET;

const method = "GET";
const requestURI = "/transaction?page=1&per_page=20";
const rawBody = "";
const timestamp = Math.floor(Date.now() / 1000).toString();

const canonicalString = `${timestamp}.${method}.${requestURI}.${rawBody}`;

const signature = crypto
  .createHmac("sha256", signingSecret)
  .update(canonicalString)
  .digest("hex");

const response = await fetch(`https://api.useoval.com${requestURI}`, {
  method,
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "X-Oval-Timestamp": timestamp,
    "X-Oval-Signature": signature
  }
});

Example: Python

import os
import time
import hmac
import hashlib
import requests

api_key = os.environ["GRAPH_API_KEY"]
signing_secret = os.environ["GRAPH_SIGNING_SECRET"]

method = "GET"
request_uri = "/transaction?page=1&per_page=20"
raw_body = ""
timestamp = str(int(time.time()))

canonical_string = f"{timestamp}.{method}.{request_uri}.{raw_body}"

signature = hmac.new(
    signing_secret.encode("utf-8"),
    canonical_string.encode("utf-8"),
    hashlib.sha256
).hexdigest()

response = requests.get(
    f"https://api.useoval.com{request_uri}",
    headers={
        "Authorization": f"Bearer {api_key}",
        "X-Oval-Timestamp": timestamp,
        "X-Oval-Signature": signature,
    },
)

Troubleshooting

If a signed request fails authentication, check the following:

  • The API key is sent as Authorization: Bearer <api_key>.
  • The key is a v2 key.
  • The correct signing_secret is being used.
  • X-Oval-Timestamp is a Unix timestamp in seconds, not milliseconds.
  • Your server clock is accurate.
  • The HTTP method in the canonical string is uppercase.
  • The requestURI includes the query string exactly as sent.
  • The signed rawBody exactly matches the request body sent to Graph.
  • The signature is lowercase hexadecimal HMAC-SHA256.

Security Best Practices

  • Store API keys and signing secrets in a secret manager.
  • Never expose secrets in frontend code, mobile apps, public repositories, or logs.
  • Use separate keys for test and live environments.
  • Use descriptive labels so keys can be identified later.
  • Rotate keys immediately if you suspect exposure.
  • Use required signing for production server-to-server integrations.