Webhooks

This section describes the webhooks that are sent to partners' backend systems when certain events occur to entities controlled by the partner.

Webhooks perform signed HTTP POST requests about specific events to a URL of your choice.

You can find the list of IP addresses from which Coinify's system sends webhook events here: IP address list

In order to receive webhook events, you must provide Coinify with a webhook URL where the callbacks will be sent to, as well as a secret that will be used to sign the webhook payloads. The secret should be shared via secure channels like LastPass. If you prefer, the secret can be generated on Coinify's side and then shared with you.

Webhook structure


This section describes the general structure of the webhooks that you will receive.

// Example webhook payload
{
  "id": "bd21c0e7-ddb6-4f8e-9367-a6ca00eca25c",
  "time": "2017-09-14T09:07:11.335Z",
  "event": "identification-attempt.approved",
  "context": {
    "traderId": "420"
  }
}

All webhooks are sent as JSON, and shares the same general structure as described in the following table:

KeyTypeDescription
idString (UUID v4)Unique identifier for the event. Retries for the same events will share the same uuid.
timeString (ISO-8601 timestamp)Timestamp for when the event has occurred.
eventStringEvent that occurred. See Webhook events for a list of possible events.
contextObjectContext for this event. Structure is defined by the event as described in [Webhook events].(#webhook-events).

Webhook events


This section provides a list of possible events that can be received as webhooks

Context of identification-attempt.* events

{
  "traderId": "1",
  "rejectReason": "DENIED" // Only for rejected event
}

Context of trade events (trade.rejected, trade.cancelled, trade.expired)

{
  "id": "1235",
  "traderId": "123",
  "residenceCountry": "DK",
  "partnerContext": {
    "clientId": 123,
    "refId": 12
  }
}

Context of trade events (trade.created, trade.transfer-in-completed, trade.completed)

{
  "id": "1235",
  "traderId": "123",
  "residenceCountry": "DK",
  "eurAmount": 100,
  "transferIn": {
    "medium": "card",
    "amount": {
      "amount": 103,
      "currency": "EUR"
    },
    "totalFee": {
      "amount": 3,
      "currency": "EUR"
    }
  },
  "transferOut": {
    "medium": "blockchain",
    "amount": {
      "amount": 0.099,
      "currency": "BTC",
      "isApproximate": false // If this is set, we cannot guarantee the amount or fee
    },
    "totalFee": {
      "amount": 0.0001,
      "currency": "BTC"
    },
    "details": {
      "address": "btc-address",
      "transaction": "btc-tx-id"
    }
  },
  "partnerContext": {
    "clientId": 123,
    "refId": 12
  }
}

Context of otc-trade.completed event

{
  "id": "1234-1234",
  "time": "yyyy-mm-ddThh:mm:ss.msZ",
  "event": "otc-trade.completed",
  "context": {
    "traderId": "123",
    "residenceCountry": "DK",
    "id": "1234",
    "partnerFee": {
      "amount": {
        "amount": 100.00,
        "currency": "EUR"
      }
    },
    "totalFee": {
      "amount": {
        "amount": 200.00,
        "currency": "EUR"
      }
    },
    "transferIn": {
      "amount": {
        "amount": 30000,
        "currency": "EUR"
      },
      "details": {
        "address": "abcd",
        "transaction": "efgh"
      },
      "transactionTime": "yyyy-mm-ddThh:mm:ss.msZ"
    },
    "transferOut": {
      "amount": {
        "amount": 1.000,
        "currency": "BTC"
      },
      "details": {
        "address": "ijkl",
        "transaction": "mnop"
      },
      "transactionTime": "yyyy-mm-ddThh:mm:ss.msZ"
    },
    "createTime": "yyyy-mm-ddThh:mm:ss.msZ"
  }
}
EventDescription
identification-attempt.approvedIdentification attempt has been approved
identification-attempt.rejectedIdentification attempt has been rejected. See Identification attempt rejection reason for possible reject reasons.
trade.createdTrade has been created
trade.completedTrade has been completed
trade.transfer-in-completedTrade transfer in completed and waiting for transfer out to be sent
trade.cancelledTrade has been cancelled
trade.expiredTrade has been expired
trade.rejectedTrade has been rejected
otc-trade.completedOTC trade has been completed. The only event possible for OTC trades.

How to respond to a webhook request


This section describes how your system should respond to an incoming webhook request

If you respond with a 2xx code, our system will consider the webhook as successfully sent and received.

If you respond with another status code than 2xx, our system will consider the webhook request as a failure and retry at a later time. See Retrying failed requests for more information about the retry strategy.

If the webhook signature is incorrect, you should consider replying just as you would if the signature was correct, to avoid attackers being able to brute-force the shared secret.

Retrying failed requests


This section describes how failed webhook requests will be retried

We will use exponential backoff for handling webhook retries.

Retry attemptsRetry Interval
1616 sec

And use this formula to calculate next retry attempt, which will result in last attempt after ~6 days from first failure.

next_retry_attempt = retry_interval * 2^retry_count

Webhook signature

Node pseudo-code to validate signature

const crypto = require('crypto');

const sharedSecret = 'shared-secret';

// Express example
const body = req.body;
const signature = req.headers['X-Coinify-Webhook-Signature'];

const hash = crypto.createHmac('sha256', sharedSecret)
  .update(JSON.stringify(body))
  .digest('hex');

return hash === signature;

Python pseudo-code to validate signature

import hashlib, hmac

shared_secret = 'the_shared_secret'

# Get the raw HTTP POST body (JSON object encoded as a string)
body = get_body()

# Get the signature from the HTTP or email headers
signature = get_header("X-Coinify-Webhook-Signature")

expected_signature = hmac.new(shared_secret, msg=body, digestmod=hashlib.sha256).hexdigest()
return signature == expected_signature

All webhooks sent from Coinify are signed with a shared secret that is known only by you and Coinify. This ensures the integrity of the data contained in the webhook, and also proves that Coinify is the sender of the webhook (provided the shared secret is not known by anyone else).

Specifically, the signature uses HMAC-SHA256, using the shared secret as the key and the full HTTP request body (UTF-8 encoded) as the message. The resulting signature is provided in lowercase hexadecimal format in the X-Coinify-Webhook-Signature HTTP header.

Signature example

Use the following example to test that your signature validation function is working correctly

Shared secret: my-shared-secret, Payload: {"examplePayload":true}

Expected signature: bcdbb89e3031905f3cc1a20d16b5f969a17a7d8fa0c26e4a807c2193402d66f4