ezForex sends signed JSON events to active HTTPS endpoints that subscribe to an event.
Every request has a common envelope. Event-specific fields are nested under data.
{
"id": "f8e44bb0-0b6f-4d2d-838d-a63ce746bb65",
"event": "rates.imported",
"created_at": "2026-07-18T19:15:32+00:00",
"data": {
"run_id": 184,
"import_id": 91
}
}
| Field | Type | Description |
|---|---|---|
| id | UUID | Stable event identifier stored with the delivery. |
| event | string | One of the event names documented below. |
| created_at | ISO 8601 datetime | UTC time at which the delivery was created. |
| data | object | The event-specific payload. |
Requests include X-EzForex-Event, X-EzForex-Delivery, X-EzForex-Timestamp, and X-EzForex-Signature headers. The timestamp is a Unix timestamp generated for the delivery attempt.
rates.importedSent to every active subscribed endpoint after an exchange-rate import completes successfully.
| Data field | Type | Description |
|---|---|---|
| run_id | integer | Import run identifier. |
| import_id | integer | Created rate snapshot identifier. |
| provider | string | Winning rate-provider slug. |
| fallback_used | boolean | Whether a provider before the winner failed. |
| provider_attempt_count | integer | Number of providers attempted. |
| provider_timestamp | integer | Unix timestamp supplied by the rate provider. |
| rates_imported | integer | Number of rates saved. |
| rates_skipped | integer | Number of provider rates skipped. |
"data": {
"run_id": 184,
"import_id": 91,
"provider": "frankfurter-ecb",
"fallback_used": true,
"provider_attempt_count": 2,
"provider_timestamp": 1784401200,
"rates_imported": 169,
"rates_skipped": 2
}
rates.import_failedSent to every active subscribed endpoint when an exchange-rate import fails.
| Data field | Type | Description |
|---|---|---|
| run_id | integer | Failed import run identifier. |
| import_id | null | No snapshot is created when all providers fail. |
| provider | string or null | Last attempted provider slug. |
| provider_attempt_count | integer | Number of providers attempted. |
| error_count | integer | Number of errors recorded for the run. |
| error_message | string or null | Summary of the import failure, when available. |
| finished_at | ISO 8601 datetime or null | Time at which the failed run finished. |
"data": {
"run_id": 185,
"import_id": null,
"provider": "frankfurter-ecb",
"provider_attempt_count": 2,
"error_count": 2,
"error_message": "Every configured exchange-rate provider failed.",
"finished_at": "2026-07-18T19:20:08+00:00"
}
rates.staleSent once per stale-data incident to every active subscribed endpoint. Internal provider errors and notification details are never included.
| Data field | Type | Description |
|---|---|---|
| incident_id | integer | Stable stale-episode identifier. |
| state | string | stale or unavailable. |
| import_id / run_id | integer or null | Related snapshot and acquisition run identifiers. |
| provider | string or null | Rate provider slug, when a snapshot exists. |
| rates_as_of | ISO 8601 datetime or null | Provider market timestamp. |
| stale_since / detected_at | ISO 8601 datetime or null | Threshold crossing and incident detection times. |
| threshold_seconds | integer | Configured freshness threshold. |
"data": {
"incident_id": 12,
"state": "stale",
"import_id": 91,
"run_id": 184,
"provider": "open-exchange-rates",
"rates_as_of": "2026-07-18T16:00:00+00:00",
"stale_since": "2026-07-18T19:00:00+00:00",
"detected_at": "2026-07-18T19:05:00+00:00",
"threshold_seconds": 10800
}
alert.triggeredSent only to active subscribed endpoints owned by the user whose rate alert was triggered. The alert must use the webhook channel.
| Data field | Type | Description |
|---|---|---|
| occurrence_id | integer | Recorded trigger occurrence identifier. |
| alert_id | integer | Rate alert identifier. |
| from | string | Three-letter source currency code. |
| to | string | Three-letter target currency code. |
| direction | string | up, down, or either. |
| threshold_pct | number | Configured percentage threshold. |
| previous_rate | number | Cross-rate in the previous distinct snapshot. |
| current_rate | number | Cross-rate in the current snapshot. |
| change_pct | number | Signed percentage movement from the previous rate. |
| previous_import_id | integer | Previous snapshot identifier. |
| current_import_id | integer | Current snapshot identifier. |
"data": {
"occurrence_id": 42,
"alert_id": 17,
"from": "USD",
"to": "ZAR",
"direction": "up",
"threshold_pct": 2,
"previous_rate": 18,
"current_rate": 18.36,
"change_pct": 2,
"previous_import_id": 90,
"current_import_id": 91
}
rates.staleThis event can be selected when configuring an endpoint, but stale-rate monitoring is not currently emitted and its data contract has not yet been defined.
Compute HMAC-SHA256 over X-EzForex-Timestamp, a period, and the exact raw request body. Compare it in constant time with the hex value after sha256= in X-EzForex-Signature.
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
if (! hash_equals($expected, $providedSignature)) {
abort(401);
}
Deliveries retry connection errors and HTTP 408, 429, and 5xx responses with exponential backoff. Other 4xx responses are terminal. Redeliveries receive a new delivery ID and retain an audit link to the original.