Onerway
Get Started

Request signing

Learn how Onerway authenticates API requests with request signatures, how to generate the sign field, and how to verify webhook notifications.

Onerway authenticates API requests using the sign field. Generate the sign dynamically on your server using the environment-specific SECRET. Never expose the SECRET in frontend code, mobile applications, or public repositories.

Before sending server-side API requests, you must also configure the IP allowlist for the target environment. Configure sandbox and production allowlists separately.

Signing rules

Generate the sign dynamically using the following rules:

  1. Exclude the sign field.
  2. Exclude fields with null, undefined, or empty string values.
  3. Serialize object (Object) and array (Array) values into compact JSON strings (without extra spaces or line breaks); see Object and array fields for nested structures.
  4. Sort field names in ascending ASCII order.
  5. Concatenate only the sorted field values directly. Do not include field names, equals signs (=), ampersands (&), or any other separators. The resulting string is called the Canonical String.
  6. Append the SECRET to the end of the Canonical String.
  7. Compute the SHA-256 hash of the final string and output the result as a lowercase hexadecimal string.

Non-string scalars are concatenated as strings: booleans become true / false; numbers use their decimal string form without trailing zeros or scientific notation (for example 1.10 is concatenated as 1.1).

Object and array fields

Object and array fields must be serialized into compact JSON strings:

{
  "billingInformation": {
    "country": "US",
    "email": "customer@test.com"
  }
}

After conversion, the value used for signing and sending is:

{
  "billingInformation": "{\"country\":\"US\",\"email\":\"customer@test.com\"}"
}

Nested values are serialized from the inside out. For example, txnOrderMsg.products is an array, so it becomes a JSON string inside the txnOrderMsg string:

{
  "txnOrderMsg": "{\"appId\":\"replace_with_app_id\",\"products\":\"[{\\\"currency\\\":\\\"USD\\\",\\\"name\\\":\\\"test product\\\",\\\"num\\\":\\\"1\\\",\\\"price\\\":\\\"1\\\"}]\",\"returnUrl\":\"https://developers.onerway.com/example-return\"}"
}

Request example

The original request can retain object structures with an empty sign field:

{
  "merchantNo": "replace_with_merchant_no",
  "merchantTxnId": "replace_with_unique_transaction_id",
  "merchantTxnTime": "2026-04-24 15:37:39",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "billingInformation": {
    "country": "US",
    "email": "customer@test.com",
    "province": "CA"
  },
  "txnOrderMsg": {
    "appId": "replace_with_app_id",
    "products": [
      {
        "currency": "USD",
        "name": "test product",
        "num": "1",
        "price": "1"
      }
    ],
    "returnUrl": "https://developers.onerway.com/example-return"
  },
  "productType": "CARD",
  "subProductType": "DIRECT",
  "txnType": "SALE",
  "sign": null
}

Before sending, convert object values to strings and fill in the calculated sign:

{
  "billingInformation": "{\"country\":\"US\",\"email\":\"customer@test.com\",\"province\":\"CA\"}",
  "merchantNo": "replace_with_merchant_no",
  "merchantTxnId": "replace_with_unique_transaction_id",
  "merchantTxnTime": "2026-04-24 15:37:39",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "productType": "CARD",
  "sign": "replace_with_calculated_signature",
  "subProductType": "DIRECT",
  "txnOrderMsg": "{\"appId\":\"replace_with_app_id\",\"products\":\"[{\\\"currency\\\":\\\"USD\\\",\\\"name\\\":\\\"test product\\\",\\\"num\\\":\\\"1\\\",\\\"price\\\":\\\"1\\\"}]\",\"returnUrl\":\"https://developers.onerway.com/example-return\"}",
  "txnType": "SALE"
}

You may sort the final request body by field name for easier troubleshooting. Signature validity strictly depends on the sorting and concatenation rules, not on the displayed body order.

Worked example

Use this minimal request with the example secret example_secret to check your implementation:

{
  "merchantNo": "demo_merchant_no",
  "merchantTxnId": "demo_txn_id_001",
  "orderAmount": "1",
  "orderCurrency": "USD",
  "sign": null
}
Canonical String: demo_merchant_nodemo_txn_id_0011USD
String to hash:   demo_merchant_nodemo_txn_id_0011USDexample_secret
sign:             8af506acd9322fcab9d676dd9b861512f194d9ab724eacdb358d90d087d9caac

Code examples

The examples below assume the input is a server-side map (Map) or object (Object); if a field is already a JSON string, ensure it has been serialized with the same inside-out rule. Use the normalizeValue result for both signing and the final request body; do not rebuild object or array fields with a different serializer after calculating the signature — this causes verification failures on the gateway.

<?php

function is_assoc_array(array $value): bool {
    if ($value === []) {
        return false;
    }
    return array_keys($value) !== range(0, count($value) - 1);
}

function normalize_nested($value) {
    if (!is_array($value)) {
        return $value;
    }

    if (!is_assoc_array($value)) {
        return array_map('normalize_nested', $value);
    }

    $result = [];
    foreach ($value as $key => $child) {
        $result[$key] = is_array($child)
            ? json_encode(normalize_nested($child), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
            : $child;
    }
    return $result;
}

function normalize_value($value) {
    if ($value === null || $value === '') {
        return $value;
    }
    if (is_bool($value)) {
        return $value ? 'true' : 'false';
    }
    return is_array($value)
        ? json_encode(normalize_nested($value), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        : (string) $value;
}

function generate_signature(array $payload, string $secret): string {
    $normalized = [];
    foreach ($payload as $key => $value) {
        if ($key !== 'sign') {
            $normalized[$key] = normalize_value($value);
        }
    }

    ksort($normalized, SORT_STRING);

    $canonical = '';
    foreach ($normalized as $value) {
        if ($value !== null && $value !== '') {
            $canonical .= $value;
        }
    }

    return hash('sha256', $canonical . $secret);
}

Webhook signature verification

Profit share, reversal, and Ethoca / RDR enrollment status notifications use the body sign field for verification. Other notifications use the X-Rh-Signature header described below. See each webhook’s API Reference for the fields that participate in its signature.

Profit share and reversal notifications

Use the SECRET for the current environment to compute the received notification’s signature following the signing rules, then compare it with the body sign. Exclude sign itself. Include relatedTxnId and relatedMerchantTxnId, excluding them when their value is null as specified by the signing rules. Use the raw receivers JSON string exactly as received; do not parse and re-serialize it. See the Profit share result webhook for the complete payload.

The /profit/query response also returns data.sign, but merchants do not need to verify query response signatures.

Ethoca / RDR enrollment status notifications

Use the SECRET for the current environment to compute the received notification’s signature following the signing rules, then compare it with the body sign. Exclude sign itself; all other fields follow the general signing rules, including the exclusion of null and empty-string values.

The notification sends id as a JSON number, which can exceed JavaScript’s safe integer range. Parse the payload with a parser that preserves large integers without losing precision, and retain the original decimal value for signature verification and acknowledgement. Do not first convert it to a JavaScript number that may lose precision.

See the Ethoca enrollment status webhook and RDR enrollment status webhook for the complete payloads and acknowledgement requirements. Ethoca alert notifications continue to use the header verification described below.

Other notifications

Onerway carries webhook signatures in the X-Rh-Signature notification header: one or more comma-separated v1=<signature> entries, where v1 identifies the signature scheme version. When multiple keys are active at the same time (for example during key rotation), each active key produces one entry:

X-Rh-Signature: v1=4bde57c3350c402ca8c3728697cd5d7dbd78c8f0313761607e7f538be3349c39, v1=6b543c2e887e20a98dd4629242dd60050a4cfdda3372cdc17b4422c8fe98d5fa

To verify, compute the signature of the received notification with your currently configured SECRET following the signing rules; verification passes if the result matches any entry in X-Rh-Signature. Field selection and concatenation follow the same rules, with two differences:

  • Also exclude the fields marked as excluded from the signature in the webhook's API Reference "Signature coverage" section, even when the notification returns values for them. Notification fields added in the future participate by default.
  • Object and array fields arrive as compact JSON strings; use the raw string values exactly as received — do not re-parse and re-serialize them.

The notification body still returns a sign field, but it is computed with the first active key only and may differ from the signature you calculate using your configured SECRET during key rotation. For these notifications, verify against X-Rh-Signature rather than the body sign. For notifications that use the body sign, follow the corresponding section above.

Common mistakes

  • Failing to serialize object (Object) or array (Array) fields into compact JSON strings.
  • Leaving txnOrderMsg.products as an array object instead of a JSON string inside the txnOrderMsg string.
  • Sending fields that are not defined for the endpoint: the gateway computes the signature over the fields defined by the API, so extra fields enter only your calculation and cause a mismatch; field structures that deviate from the documentation fail the same way.
  • Environment mismatch, such as using a sandbox merchantNo / SECRET with the production API, or production credentials with the sandbox API.

When you need support

When request signing fails, print and provide the following server-side information:

  • The final concatenated string before signature calculation (Canonical String).
  • The generated sign.
  • The request environment: sandbox or production.
  • The API endpoint and full request body. You may mask sensitive information, but do not modify the values of the fields participating in the signature.

When webhook verification fails, provide the full notification payload as received, the body sign or X-Rh-Signature specified by the webhook’s API Reference, and the Canonical String and signature you computed.