# World Radio League API > Log amateur radio contacts into a World Radio League logbook, and read them back. > Version 1.1.0. Full machine-readable spec: https://api.worldradioleague.com/v1/openapi.json ## Getting started 1. The operator generates a key in World Radio League under Integrations > Developer API. It is shown once and stored hashed. One key covers the whole account, not per logbook. 2. Send it as `Authorization: Bearer wrl_live_...` (or `X-API-Key`). 3. Call `GET /v1/me` first to confirm the key works and see the rate limits. Base URL: https://api.worldradioleague.com ## Rules that will trip you up - Keep the key server-side. These endpoints send no CORS headers, so a browser cannot call them. An API key in a front-end bundle is a leaked key. - `POST /v1/contacts` takes ONE contact. An array is rejected with 413. For bulk uploads, send an ADIF file through `POST /v1/imports` (see Bulk import below). - `logbookId` is OPTIONAL when creating a contact. Omit it and the contact goes to the operator's default logbook, so no setup call is needed. Send it when you know which logbook you want; it must belong to the key's owner. - If the operator has several logbooks and no default, omitting `logbookId` returns 422 LOGBOOK_REQUIRED. Do not retry blindly — read `GET /v1/me` (`defaultLogbook`) at setup and ask the operator to choose, or send `logbookId` yourself. - A contact cannot be moved between logbooks. `logbookId` is rejected on PATCH. - Field names are ADIF names, camelCased: `call`, `freq`, `txPwr`, `rstRcvd`, `stationCallsign`, `gridsquare`, `qth`, `dxcc`. If you have written an ADIF importer, you already know them. Do NOT guess `callsign`, `frequency` or `power` — unknown fields are rejected. - `timestamp` is the one deliberate departure from ADIF: one ISO-8601 UTC instant instead of ADIF's QSO_DATE + TIME_ON pair. The pair is also accepted, under ADIF's own names: `"timestamp": {"qsoDate": "20260805", "timeOn": "1435"}`. - Timestamps are UTC. An ISO-8601 string with no timezone is treated as UTC, not local. - `mode` carries what ADIF splits across MODE and SUBMODE: WRL stores a single mode field, so send the submode when you want to be specific. Send "FT8", not "MFSK" plus a submode. For JS8, send "JS8" — WRL understands that as MODE = MFSK, SUBMODE = JS8. - `state` is the contacted station's state or province. Send the abbreviation where possible: "TX", not "Texas". - `gridsquare` is the contacted station's Maidenhead locator; `myGridsquare` is yours. - `name` is the contacted station operator's name. - `stationCallsign` (the station callsign) and `operator` (the callsign of whoever is operating the station) both default to the account callsign when omitted. Send them when the operating callsign differs from the account holder's. - `band` is in metres: "20m", "20" or 20; "70cm" becomes 0.7. If band and frequency disagree, the frequency wins and a warning is returned in `meta.warnings`. - Unknown request fields are REJECTED, not ignored. `grid_square` is not `gridsquare` and you will get a 400 saying so. - `programId` is REQUIRED on every contact. Send a short name for your software, e.g. "Ham2k" — it is the equivalent of ADIF PROGRAMID and records which application logged the QSO. It is not returned in responses. - Activity locations (POTA, SOTA, IOTA, WWFF) go in `myActivities` (where YOU were operating from) and `theirActivities` (where the station you worked was). Each entry is `{ "type": "POTA", "ref": "US-0662", "isPrimary": true }`; up to 7 per side. `type` may be omitted when the reference is unique — where it is not, the request is refused rather than guessed at. `isPrimary` marks the location used for the map pin and the activity breakdowns; if you flag none, the first entry is used. - A reference WRL does not know is REJECTED (422) and the contact is not created, so an activation is never stored with its park silently missing. - The arrays are the ONLY form. The ADIF tags MY_SIG/MY_SIG_INFO and SIG/SIG_INFO are NOT accepted — they hold one value each and cannot express a two-fer. Send the array even for a single activity. - A PATCH REPLACES the side it names in full: send the whole list you want to end up with, `[]` to clear that side, or omit the field to leave it alone. - A logbook that still contains contacts cannot be deleted (409). - Asking for another account's resource returns 404, never 403. ## Enrichment Enrichment is the process by which World Radio League derives missing information from the data you provide. A new contact returns `enrichment: "pending"`; within a few seconds we derive `dxcc` and `distance` from the callsign. Re-read the contact with `GET /v1/contacts/{id}` to see them. Anything you supplied yourself is never overwritten. Country, continent and flag are NOT returned — all three are derivable from `dxcc`, so they are not duplicated on every contact. ## Responses Every response is `{ "data": ..., "meta": ..., "error": ... }`. On failure `error.code` is a stable string — branch on it, never on `error.message`. Every response carries `X-Request-Id`; quote it in support requests. ## Bulk import (ADIF) Three steps: start, upload, poll. The file itself never goes to this API: 1. Start: `POST /v1/imports` with `{ "programId": "YourAppName" }` (optionally `logbookId`). Returns `data.id` and `data.upload.url`. 2. Upload: `PUT` the raw .adi file (max 10 MB) to `data.upload.url` — NO API key on that request. The URL lasts two hours; PUT again if an upload fails part-way. 3. Poll: `GET /v1/imports/{id}` every few seconds. There is NO completion call — the upload is noticed, checked and queued automatically. Stop on `complete`, `complete_with_errors`, `failed`, `rejected`, `expired` or `cancelled`. `rejected` means nothing was imported or charged; `data.errorCode` says why (FILE_TOO_LARGE, NO_RECORDS, EXCEEDS_DAILY_LIMIT, IMPORT_QUOTA_EXHAUSTED, KEY_REVOKED, LOGBOOK_DELETED). The file is processed by World Radio League's own ADIF import: duplicates are skipped and contacts are enriched exactly as for an import started in the app. One import runs at a time per account (409 otherwise). Records count against the account's `limits.importContactsPerDay` from `GET /v1/me`, a budget separate from writes; 0 means bulk import is not available. 429 on start means retry later (honour Retry-After): your tier's import backlog is full, or the account's daily start limit is reached. The membership's own import allowance, as for imports in the app, also applies: reaching it ends the import `failed` / QUOTA_EXCEEDED. ## Rate limits 60 writes/min, 120 reads/min, 5,000 writes/day, 20,000 reads/day. `429` carries `Retry-After` in seconds, computed from the window that actually blocked. Honour it rather than retrying on a fixed timer. ## Endpoints ### GET /v1/me Identity and quota for the current key ### GET /v1/contacts List your contacts ### POST /v1/contacts Log one contact ### GET /v1/contacts/{id} Fetch one contact ### PATCH /v1/contacts/{id} Correct a contact ### DELETE /v1/contacts/{id} Delete a contact ### POST /v1/imports Start an ADIF import ### GET /v1/imports/{id} Import status and counts ### GET /v1/logbooks List your logbooks ### POST /v1/logbooks Create a logbook ### GET /v1/logbooks/{id} Fetch one logbook ### PATCH /v1/logbooks/{id} Update a logbook ### DELETE /v1/logbooks/{id} Delete an empty logbook ## Minimal example ```bash curl -X POST "https://api.worldradioleague.com/v1/contacts" \ -H "Authorization: Bearer $WRL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "logbookId": "6f1c0b7e-6c2a-4c4e-9d1f-2a5b3c4d5e6f", "programId": "YourAppName", "call": "W1AW", "timestamp": "2026-08-05T14:35:00Z", "freq": 14.074, "band": "20m", "mode": "FT8" }' ``` ## Bulk import example ```bash BASE="https://api.worldradioleague.com" IMPORT=$(curl -s -X POST "$BASE/v1/imports" \ -H "Authorization: Bearer $WRL_API_KEY" -H "Content-Type: application/json" \ -d '{"programId": "YourAppName"}') ID=$(echo "$IMPORT" | jq -r .data.id) curl -X PUT "$(echo "$IMPORT" | jq -r .data.upload.url)" \ -H "Content-Type: text/plain" --data-binary @log.adi # then poll until status is complete (or rejected / failed) curl "$BASE/v1/imports/$ID" -H "Authorization: Bearer $WRL_API_KEY" ```