For the complete documentation index, see llms.txt. This page is also available as Markdown.

Callbacks

OmyPayments notifies your backend by calling your **Callback URL** whenever an entity changes status. Set the Callback URL in your account settings.

Requirements

  • Your endpoint must be publicly accessible and return a 200 HTTP status code on receipt.

  • The same endpoint receives every callback type. Route by payload shape: invoice callbacks carry usdAmount / productTitle; swap callbacks carry status in the form new | in_progress | finished | rejected together with cryptoFrom / cryptoTo.


Invoice Callbacks

Sent every time an invoice changes status.

Payload

{
    "id": "UUID", // Invoice ID
    "usdAmount": "100.00", // Invoice amount in USD
    "status": "pending", // Invoice status
    "productTitle": "Test Product", // Product title
    "extraData": null, // Additional invoice information.
    "cryptoAmount": "0", // Amount in selected cryptocurrency (or null if not chosen)
    "crypto": { // Cryptocurrency data (or null if not chosen)
        "id": "uuid", // Crypto UUID
        "title": "Tether USD", // Crypto title
        "symbol": "USDT", // Crypto symbol
        "contractAddress": "0x0000000000000000000000000000000000000000", // Crypto contract address
        "decimals": 6, // Crypto decimals
        "logoURI": "https://..." // Crypto logo URI
    },
    "network": { // Network data (or null if not chosen)
        "id": "UUID", // Network UUID
        "chainId": "56", // Network chain ID
        "symbol": "BSC", // Network symbol
        "name": "BNB Chain", // Network title
        "networkType": "evm" // Network type
    },
    "address": "0x0000000000000000000000000000000000000000", // Payment address (or null if not chosen)
    "expiredAt": "2025-01-01 00:00:00", // Invoice expiration datetime (UTC+0)
    "transfer": { // Payment transaction details (or null if not available)
        "from": "0x0000000000000000000000000000000000000000", // Transfer from address
        "hash": "0x0000000000000000000000000000000000000000000000000000000000000000" // Transfer hash
    },
    "signature": "sha256=<hex>" // HMAC-SHA256 signature - see "Verifying webhook signatures" below
}

Swap Callbacks

Sent every time a swap changes status. Each callback carries an event type in the form swap.<status>:

Event
Sent when

swap.new

A swap was created and funds were reserved.

swap.in_progress

The swap started executing on-chain / via the routing provider.

swap.finished

The swap completed successfully (amountTo, hashTo populated).

swap.rejected

The swap failed; reserved funds were returned to available balance.

Payload

Amounts (amountFrom, amountTo) are returned in the cryptocurrency's base units. Use the matching decimals value to convert to a human-readable amount. The amountTo / hashTo fields are populated once the swap reaches the finished status.


Withdrawal Callbacks

Sent every time a withdrawal changes its main status. Each callback carries an event type in the form withdrawal.<status>:

Event
Sent when

withdrawal.pending

A withdrawal was created and funds were reserved from your asset balance.

withdrawal.success

The withdrawal was sent on-chain successfully (txHash populated).

withdrawal.failed

The withdrawal was declined, or its transaction reverted / timed out; reserved funds were returned to your available balance.

Only main-status changes are delivered. Intermediate states of a withdrawal (e.g. accepted, sending, sent) do not trigger a callback, and the payload does not include a sub-status field. A given withdrawal therefore emits at most one withdrawal.pending, followed by exactly one terminal withdrawal.success or withdrawal.failed.

Payload

amount is returned in the cryptocurrency's base units — use the matching crypto.decimals value to convert to a human-readable amount. The payload is intentionally lean; for the full withdrawal details (fees, gas breakdown) query Get a withdrawal by id.


Verifying webhook signatures

Every callback includes a signature field in the root of the payload:

Verifying this field lets you confirm that the callback was sent by OmyPayments and that no field was modified in transit.

Getting your webhook secret

Find your webhook secret in the merchant dashboard on the Profile page, in the API keys section. Use the Regenerate button if you need to rotate it. Keep the secret server-side - never expose it to a browser or include it in client code.

How the signature is computed

The signature is HMAC-SHA256 over the canonicalized payload (the full callback body with the signature field removed), prefixed with sha256=:

Canonicalization algorithm:

  1. Remove the signature field from the payload.

  2. Recursively drop null values from both objects and arrays:

    • Objects: remove every key whose value is null; recurse into non-null values. Drop the entire object if all keys are removed.

    • Arrays: remove every null element; recurse into non-null elements. Element order is preserved; the array is re-indexed after removal.

  3. Serialize the result to JSON with:

    • Keys sorted by Unicode code point (ascending); all keys are ASCII, so this is plain lexicographic order.

    • Compact format - separators , and :, no spaces.

    • Strings in raw UTF-8: non-ASCII characters are not escaped to \uXXXX; only "\", \\\, and C0 control characters are escaped. The characters /, <, >, & are not escaped.

    • Integer fields (e.g., decimals) serialized as-is (6, 18); all monetary amounts are strings.

Test vectors

Use these vectors to validate your implementation. Secret for all three:

Vector 1 - invoice.completed, full payload (Unicode in productTitle, JSON string in extraData)

Canonical string:

Expected signature:

Vector 2 - invoice.pending, minimal (all nullable fields are null → dropped)

Canonical string:

Expected signature:

Vector 3 - swap.completed (hashTo is null → dropped; createdAt in RFC3339)

Canonical string:

Expected signature:

Verification examples

The following examples are verified against all three test vectors above.

Python

Usage:

Language notes: json.dumps with sort_keys=True and separators=(',',':') handles key sorting and compact format. ensure_ascii=False outputs UTF-8 directly without \uXXXX escaping. hmac.compare_digest provides constant-time comparison.


JavaScript / Node.js

Usage:

Language notes: JSON.stringify does not sort keys - the explicit recursive _sortKeys step is required. JSON.stringify does not escape non-ASCII characters by default, which matches the spec. crypto.timingSafeEqual requires equal-length buffers - the length check before the call is intentional.


PHP

Usage:

Language notes: ksort must be applied recursively - the standard json_encode does not sort keys. array_is_list distinguishes PHP lists (sequential int keys) from associative arrays: lists get array_values re-indexing after null removal, associative arrays get ksort. JSON_UNESCAPED_UNICODE outputs UTF-8 directly; JSON_UNESCAPED_SLASHES prevents / from being escaped as \/. hash_equals provides constant-time comparison.


Go

Language notes:

  • json.Decoder.UseNumber() -when decoding a JSON body into map[string]interface{}, Go maps all JSON numbers to float64 by default. UseNumber() preserves them as json.Number (the exact original string), which then serializes back to the exact original representation. Without this, "decimals":6 would round-trip correctly, but large integers or numbers with specific precision could drift.

  • enc.SetEscapeHTML(false) - Go's json.Encoder escapes <, >, & to <, >, & by default. This would produce a different canonical string than the spec. Always disable HTML escaping.

  • Manual key sorting - encoding/json does not guarantee key order when marshaling map[string]interface{}. Always sort keys explicitly before serializing.

  • Arrays ([]interface{}) - when dropNull encounters a JSON array it removes null elements and recurses into the remaining items; sortedMarshal serializes them in their original order (arrays are never re-sorted). Both functions use a switch type assertion so slices are handled explicitly rather than falling through to the scalar branch.

  • hmac.Equal - constant-time byte comparison. Do not use == or bytes.Equal to compare signatures.


Recommendations

Idempotency by invoice ID. OmyPayments does not include replay protection (timestamps, nonces). To guard against a valid callback being replayed, track which invoice IDs you have already processed and ignore duplicates:

Date formats. Invoice callbacks use "2025-01-01 00:00:00" (UTC, no timezone marker); swap callbacks use RFC3339 ("2025-03-11T14:30:16+00:00"). Both are opaque strings during canonicalization — no parsing needed for signature verification.

Last updated