REST Countries

DocsWebhooks

Subscribe one or more URLs to receive a signed POST when REST Countries data changes (e.g. when a country's leaders updates after an election, or its population shifts after a World Bank refresh). Every event fans out to every active endpoint on your account; deliveries are signed, retried on failure, and tagged with an idempotency key so safe duplicate handling is straightforward.

Webhooks are a Business plan feature. Free, Personal, and Professional plans cannot register endpoints. Business accounts can configure up to 5 active endpoints. See Plans or contact us if you'd like to discuss your use case.

Overview

REST Countries' read API tells you the current state of a country. Webhooks tell you when that state changed, so you don't have to poll. Each subscription is a URL on your infrastructure that receives a JSON POST whenever a relevant event fires on your account.

What's the same as a normal API call: JSON body, deterministic schemas pinned to a version, the v5 field shapes you already use elsewhere in the docs.

What's different: REST Countries is the client; you're the server. That flips three things: you authenticate us via signature verification (not bearer tokens), you respond fast and let async work happen after, and you treat retries as expected rather than exceptional.

Quick start

Three steps to a working endpoint.

1. Add an endpoint. Go to /webhooks, click Add a webhook, give it a label and the public URL REST Countries will POST to. REST Countries generates a signing secret unique to that endpoint; copy it and store it alongside the rest of your service secrets.

2. Receive the POST. REST Countries sends a JSON body with a top-level event, data, and created. Respond 2xx within 30 seconds. Anything else is treated as a failure and queued for retry.

3. Verify the signature. Compute HMAC-SHA256 of the raw request body using your endpoint's secret, then compare against the value in the RESTCountries-Signature header in constant time.

// Node: Express handler
const crypto = require('crypto');

app.post('/rest-countries/webhook', express.raw({ type: 'application/json' }), function(req, res) {
const signature = req.headers['RESTCountries-Signature'];
const expected = crypto.createHmac('sha256', process.env.RESTCOUNTRIES_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) === false) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// queue work, then respond fast
res.status(204).end();
});

Configuring endpoints

Endpoints are managed on the /webhooks page. Each endpoint has three properties you control and a few REST Countries manages for you.

Properties you set:

  • Label: your reference only. Never sent in delivery payloads. Editable any time.
  • URL: the HTTPS endpoint REST Countries POSTs to. Immutable after creation: the URL is part of how delivery history is tracked, so changing it would muddy the failure / retry record. To change a URL, delete the endpoint and add a new one.
  • Active: toggle to pause an endpoint without deleting it. While inactive, no events are queued for delivery and pending retries are canceled.

Properties REST Countries manages:

  • Public ID: every endpoint gets a stable identifier in the form wbhk_… that's included in delivery headers so you can correlate logs.
  • Signing secret: generated at creation time and unique per endpoint. See Secrets & rotation.
  • Attempt counters: total attempts and successful deliveries are tracked on the endpoint and surfaced on the dashboard.
HTTPS only. REST Countries will not deliver to http:// URLs in production. For local development see Testing locally.

Event types

Events are namespaced strings of the form resource.action or resource.action.detail. Every active endpoint on your account receives every event. REST Countries does not currently support per-event filtering. Filter on your side by inspecting the event field of the payload.

The current event types fired by REST Countries:

  • country.updated: one or more fields on a country changed after a reconciliation cycle. Fires once per country per cycle, batching all field deltas into a single payload.
  • country.field.changed: emitted in addition to country.updated when a watched field (e.g. leaders, currencies, capital) changes. The payload identifies the property path and includes the previous and new values.
  • country.added: a new country has been added to the dataset (rare; tied to political events like state recognition).
  • country.removed: a country has been removed (rarer; e.g. a dissolution where REST Countries' policy is to retire the record after a grace period).

New event types may be added over time. Treat any unrecognized event value as a no-op (don't reject the request), and we'll announce additions before they fire on your account.

Payload format

Every delivery is a JSON object with the same top-level shape. The data field's contents depend on the event type.

{
"id": "evt_01HXY7Z3K8M2RFJ0AB12CD34EF",
"webhook": "wbhk_01HXY7Z3K8M2RFJ0AB12CD34EF",
"event": "country.field.changed",
"created": "2026-05-08T14:22:11Z",
"api_version": "v5",
"data": {
"country": "CA",
"property": "leaders[0].name",
"previous": "Justin Trudeau",
"current": "Mark Carney",
"effective": "2026-05-08T13:00:00Z"
}
}

Top-level fields:

  • id: unique event identifier (evt_…); the same value on every retry of the same delivery.
  • webhook: the endpoint ID this delivery is going to (wbhk_…); useful when one consumer handles multiple REST Countries endpoints.
  • event: the event type (see Event types).
  • created: ISO-8601 UTC timestamp of when the event was first generated, not when this particular delivery attempt was made.
  • api_version: the API version whose schema the data field conforms to. Stays pinned to the version active when your endpoint was created. See API versions.
  • data: event-specific payload.

Request headers

Every delivery includes the following request headers:

Header Description
Content-Type Always application/json.
User-Agent Identifies REST Countries as the sender, e.g. RESTCountries-Webhooks/1.0.
RESTCountries-Signature HMAC-SHA256 of the raw request body, hex-encoded. See Signature verification.
RESTCountries-Event Mirror of the event field in the body. Useful for routing without parsing JSON.
RESTCountries-Webhook-Id The endpoint's public ID (wbhk_…).
RESTCountries-Idempotency-Key The event ID. Identical across retries of the same delivery. See Idempotency.
RESTCountries-Delivery-Attempt 1-indexed attempt counter (1 on first delivery; 25 on retries).

Signature verification

REST Countries signs every delivery with the endpoint's signing secret so you can confirm a request really came from us and the body wasn't tampered with in transit. Verify on every request. Without verification, anyone who learns your endpoint URL can POST to it.

Algorithm: HMAC-SHA256 over the raw request body, hex-encoded. The secret is the per-endpoint signing secret you copied at creation.

Critical: verify against the raw, unmodified request body. JSON re-serialization, whitespace normalization, or middleware that re-encodes the body will produce a different digest and every signature will fail. Read the body as bytes, verify, then parse.

Always compare in constant time to avoid timing oracles.

// PHP: handler
function verifyRESTCountriesSignature(string $rawBody, string $signatureHeader, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}

$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_RESTCOUNTRIES_SIGNATURE'] ?? '';
if (verifyRESTCountriesSignature($rawBody, $signature, getenv('RESTCOUNTRIES_WEBHOOK_SECRET')) === false) {
http_response_code(401);
exit;
}
// Python: Flask handler
import hmac, hashlib, os
from flask import request, abort

def verify_rc_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

@app.post('/rest-countries/webhook')
def handle():
if verify_rc_signature(request.get_data(), request.headers.get('RESTCountries-Signature', ''), os.environ['RESTCOUNTRIES_WEBHOOK_SECRET']) is False:
abort(401)
return '', 204

Delivery & retries

Method: POST with a JSON body, sent to the configured endpoint URL.

Timeout: REST Countries waits up to 30 seconds for a response. Treat this as a hard ceiling; your handler should acknowledge fast and move slow work onto a queue.

What counts as success: any HTTP status in the 2xx range. 200, 202, and 204 are all fine; pick whichever fits your stack.

What counts as failure: connection refused, DNS failure, TLS handshake failure, timeout, or any non-2xx status. 3xx is treated as failure; REST Countries does not follow redirects.

Retry policy: up to 4 retries (5 attempts total) with exponential backoff. Approximate schedule from the original event timestamp:

Attempt When
1Immediately when the event fires
2~1 minute after attempt 1
3~5 minutes after attempt 2
4~30 minutes after attempt 3
5~2 hours after attempt 4

After the 5th failed attempt the event is dropped and the endpoint is flagged as failing in the dashboard. REST Countries does not currently expose a dead-letter queue.

Idempotency

Retries are normal. Network blips, deploys, and brief 5xx responses all cause the same event to land at your handler twice. Build for it.

The RESTCountries-Idempotency-Key request header (and the matching id field in the body) is identical across all attempts of the same delivery. Persist that key when you process an event and skip if you've seen it before. A short-TTL cache backed by Redis or a unique constraint on a database table both work. REST Countries' retry window caps at ~3 hours from the original event, so storage of a few hours is plenty.

Across different endpoints on the same account, the same event will have a different delivery but the same id. If multiple endpoints feed the same handler, dedupe on id alone, not the endpoint-scoped attempt header.

Testing locally

REST Countries will not deliver to localhost or other private hosts; the connection won't reach you. Two patterns work for local development:

Tunnel a public URL to your dev machine. Tools like ngrok or Cloudflare Tunnel give you an HTTPS URL that proxies to a port on your machine. Register that URL as a webhook endpoint, then iterate locally.

Use the Send test button. The dashboard fires a synthetic event of each event type to a single endpoint without waiting for a real upstream change. Useful for verifying signature handling and your handler's branching logic.

Test events are clearly marked. Test deliveries set "test": true on the top-level payload so you can route them away from production side effects (charges, emails, downstream calls).

Secrets & rotation

Each endpoint has its own signing secret, generated when you add the endpoint. The full secret value is shown once at creation; afterwards the dashboard shows a masked view and a copy-on-reveal action. Store the secret with the rest of your service credentials (environment variable, secrets manager, whatever you already use).

Different secret per endpoint. A production receiver and a staging receiver have independent secrets, so leaking one never compromises the other.

Rotation. In-place secret rotation is on the roadmap. Today, rotate by adding a second endpoint pointing at the same URL with a new secret, deploying handler code that accepts either signature, then deleting the old endpoint. Reach out via support if you'd prefer us to cycle a secret on your behalf.

Failure handling

When an endpoint accumulates consecutive failed deliveries, the dashboard surfaces a Failing badge with the most recent error and the next retry time. Endpoints are not automatically disabled; repeated failures keep retrying on the schedule above and fresh events keep queueing.

If your endpoint is going to be down for a known maintenance window, toggle it inactive on the /webhooks page rather than letting deliveries fail. Inactive endpoints don't accrue retries, and reactivating doesn't replay the events you missed during the window.

To replay missed events, contact support with the endpoint and the time range. REST Countries keeps event records for 30 days and we can re-issue deliveries from that history.