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:
Key | Type | Description |
---|---|---|
id | String (UUID v4) | Unique identifier for the event. Retries for the same events will share the same uuid . |
time | String (ISO-8601 timestamp) | Timestamp for when the event has occurred. |
event | String | Event that occurred. See Webhook events for a list of possible events. |
context | Object | Context 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"
}
}
Event | Description |
---|---|
identification-attempt.approved | Identification attempt has been approved |
identification-attempt.rejected | Identification attempt has been rejected. See Identification attempt rejection reason for possible reject reasons. |
trade.created | Trade has been created |
trade.completed | Trade has been completed |
trade.transfer-in-completed | Trade transfer in completed and waiting for transfer out to be sent |
trade.cancelled | Trade has been cancelled |
trade.expired | Trade has been expired |
trade.rejected | Trade has been rejected |
otc-trade.completed | OTC 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 attempts | Retry Interval |
---|---|
16 | 16 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