Access to the verification server API requires an API key. An API key typically corresponds to an individual mobile application or individual human. There are two types of API keys:
-
DEVICE
- Intended for a mobile application to call thecmd/apiserver
to perform the two step protocol to exchange verification codes for verification tokens, and verification tokens for verification certificates. -
ADMIN
- Intended for public health authority internal applications to integrate with this server. We strongly advise putting additional protections in place such as an external proxy authentication. -
STATS
- Intended for public health authorities to gather automated statistics.
The following APIs exist for the API server (cmd/apiserver
). All APIs are JSON
over HTTP. You should always specify the content-type
and accept
headers as
application/json
. Check with your server operator for the specific hostname.
All endpoints require an API key passed via the X-API-Key
header. The server
supports HTTP/2, so the header key is case-insensitive. For example:
curl https://example.encv.org/api/method \
--header "content-type: application/json" \
--header "accept: application/json" \
--header "x-api-key: abcd.5.dkanddssk"
API keys will generally be in a particular format, but developers should not attempt to build any intelligence on this format. The format, length, and character set are not guaranteed to remain the same between releases.
All errors contain an English language error message and well defines ErrorCode
.
The ErrorCodes
are defined in api.go.
Exchange a verification code for a long term verification token.
VerifyCodeRequest
{
"code": "<the code>",
"accept": ["confirmed"],
"padding": "<bytes>"
}
accept
is an optional list of the diagnosis types that the client is willing to process. Accepted values are["confirmed"]
["confirmed", "likely"]
["confirmed", "likely", "negative"]
- It is not possible to get just
likely
or justnegative
- if a client passeslikely
they are indicating they can process bothconfirmed
andlikely
.
padding
is a recommended field that obfuscates the size of the request body to a network observer. The client should generate and insert a random number of base64-encoded bytes into this field. The server does not process the padding.
VerifyCodeResponse
{
"testtype": "<test type string>",
"symptomDate": "YYYY-MM-DD",
"testDate": "YYYY-MM-DD",
"token": "<JWT verification token>",
"error": "",
"errorCode": "",
"padding": "<bytes>"
}
symptomDate
andtestDate
will be present of that information was provided when the verification code was generated. These fields are omitted in the response body if corresponding date was not set.padding
is a field that obfuscates the size of the response body to a network observer. The server may generate and insert a random number of base64-encoded bytes into this field. The client should not process the padding.
Possible error code responses. New error codes may be added in future releases.
ErrorCode | HTTP Status | Retry | Meaning |
---|---|---|---|
unparsable_request |
400 | No | Client sent an request the sever cannot parse |
code_invalid |
400 | No | Code invalid or used, user may need to obtain a new code. |
code_expired |
400 | No | Code has expired, user may need to obtain a new code. |
code_not_found |
400 | No | The server has no record of that code. |
invalid_test_type |
400 | No | The client sent an accept of an unrecognized test type |
maintenance_mode |
429 | Yes | The server is temporarily down for maintenance. Wait and retry later. |
quota_exceeded |
429 | Yes | The realm has run out of its daily quota allocation for issuing codes. Wait and retry later. |
500 | Yes | Internal processing error, may be successful on retry. |
Exchange a verification token for a verification certificate (for sending to a key server)
VerificationCertificateRequest
{
"token": "token from verifyCodeResponse",
"ekeyhmac": "hmac of exposure keys, base64 encoded",
"padding": "<bytes>"
}
token
: must be exactly the string that was returned on the/api/verify
requestekeyhmac
: must be calculated on the client- The client generates an HMAC secret and calculates the HMAC of the actual TEK data
- Plaintext generation algorithm
- Sample HMAC generation (Go)
- The key server will re-calculate this HMAC and it MUST match what is presented here.
padding
is a recommended field that obfuscates the size of the request body to a network observer. The client should generate and insert a random number of base64-encoded bytes into this field. The server does not process the padding.
VerificationCertificateResponse
{
"certificate": "<JWT verification certificate>",
"error": "",
"errorCode": "",
"padding": "<bytes>"
}
padding
is a field that obfuscates the size of the response body to a network observer. The server may generate and insert a random number of base64-encoded bytes into this field. The client should not process the padding.
Possible error code responses. New error codes may be added in future releases.
ErrorCode | HTTP Status | Retry | Meaning |
---|---|---|---|
token_invalid |
400 | No | The provided token is invalid, or already used to generate a certificate |
token_expired |
400 | No | Code invalid or used, user may need to obtain a new code. |
hmac_invalid |
400 | No | The ekeyhmac field, when base64 decoded is not the right size (32 bytes) |
maintenance_mode |
429 | Yes | The server is temporarily down for maintenance. Wait and retry later. |
500 | Yes | Internal processing error, may be successful on retry. |
These APIs are available on the admin server and require and ADMIN
level API key.
Request a verification code to be issued. Accepts [optional] symptom date and test dates in ISO 8601 format. These can be in local time, if a timezone offset is provided. If a phone number is provided and the realm is configured with SMS credentials, then an SMS will be dispatched according to the realm's settings.
IssueCodeRequest
{
"symptomDate": "YYYY-MM-DD",
"testDate": "YYYY-MM-DD",
"testType": "<valid test type>",
"tzOffset": 0,
"phone": "+CC Phone number",
"smsTemplateLabel": "my sms template",
"padding": "<bytes>",
"uuid": "optional string UUID",
"externalIssuerID": "external-ID",
}
-
sypmtomDate
andtestDate
are both optional.- only one will be encoded into the eventually issued certificate
- symptom date is always preferred to test date
-
testType
- Must be
confirmed
,likely
,negative
- valid values depends on your realm's settings
- Must be
-
tzOffset
- Offset in minutes of the user's timezone. Positive, negative, 0, or omitted (using the default of 0) are all valid. 0 is considered to be UTC.
-
phone
- Phone number to send the SMS to. If a phone number is provided, but the SMS text message fails to send, the API will return a 4xx client error.
-
smsTemplateLabel
- If the realm has more than one SMS template defined, this may be optionally specify the label of the message template which the server should compose. If omitted, the default template will be used.
-
padding
is a recommended field that obfuscates the size of the request body to a network observer. The client should generate and insert a random number of base64-encoded bytes into this field. The server does not process the padding. -
uuid
is optional as request input. The server will generate a uuid on response if omitted.- This is a handle which allows the issuer to track status of the issued verification code.
-
externalIssuerID
is an optional field supplied by the API caller to uniquely identify the entity making this request. This is useful where callers are using a single API key behind an ERP, or when callers are using the verification server as an API with a different authentication system. This field is optional.- The information provided is stored exactly as-is. If the identifier is uniquely identifying PII (such as an email address, employee ID, SSN, etc), the caller should apply a cryptographic hash before sending that data. The system does not sanitize or encrypt these external IDs, it is the caller's responsibility to do so.
IssueCodeResponse
{
"uuid": "string UUID",
"code": "short verification code",
"expiresAt": "RFC1123 formatted string timestamp",
"expiresAtTimestamp": 0,
"expiresAt": "RFC1123 UTC timestamp",
"expiresAtTimestamp": 0,
"longExpiresAt": "RFC1123 UTC timestamp",
"longExpiresAtTimestamp": 0,
"error": "descriptive error message",
"errorCode": "well defined error code from api.go",
}
uuid
- UUID is a handle which allows the issuer to track status of the issued verification code.
code
- The OTP code which may be exchanged by the user for a signing token.
expiresAt
- RFC1123 formatted string timestamp, in UTC. After this time the code will no longer be accepted and is eligible for deletion.
expiresAtTimestamp
- represents Unix, seconds since the epoch. Still UTC. After this time the code will no longer be accepted and is eligible for deletion.
longExpiresAt
- represents the time that the link containing a 'long' verification code expires (if one was issued)
longExpiresAtTimestamp
- Unix, seconds since the epoch for
longExpiresAt
- Unix, seconds since the epoch for
padding
is a field that obfuscates the size of the response body to a network observer. The server may generate and insert a random number of base64-encoded bytes into this field. The client should not process the padding.
Possible error code responses. New error codes may be added in future releases.
ErrorCode | HTTP Status | Retry | Meaning |
---|---|---|---|
unparsable_request |
400 | No | Client sent an request the sever cannot parse |
invalid_test_type |
400 | No | The client sent an accept of an unrecognized test type |
missing_date |
400 | No | The realm requires either a test or symptom date, but none was provided. |
invalid_date |
400 | No | The provided test or symptom date, was older or newer than the realm allows. |
invalid_test_type |
400 | No | The test type is not a valid test type (a string that is unknown to the server). |
uuid_already_exists |
409 | No | The UUID has already been used for an issued code |
maintenance_mode |
429 | Yes | The server is temporarily down for maintenance. Wait and retry later. |
quota_exceeded |
429 | Yes | The realm has run out of its daily quota allocation for issuing codes. Wait and retry later. |
unsupported_test_type |
412 | No | The code may be valid, but represents a test type the client cannot process. User may need to upgrade software. |
500 | Yes | Internal processing error, may be successful on retry. |
Every response includes uuid
to track the status of an issued code. Optionally IssueCodeRequest
may also take in a uuid
from the client. This can be useful when implementing retry logic to ensure the same request does not send more than one SMS to the same patient. Once successful, subsequent requests with the same uuid
will give status 409
uuid_already_exists
.
The uuid
field is stored on the server for tracking. It is therefore important that this field remains meaningless to the server. The client may generate them randomly and store them locally, or use a one-way hash of phone
using a locally known HMAC key.
This may also be used as an external handle to coordinate among multiple external issuers. For example, a testing lab which issues codes might attach a uuid
to case information before handing off data to the state or other agencies to prevent multiple notifications to the patient.
Request a batch of verification codes to be issued. Accepts a list of IssueCodeRequest. See /api/issue
for details of the fields of a single issue request and response. The indices of the respective
codes
arrays will match each request/response pair unless a server error occurs which results in an empty codes
array response.
This API currently supports a limit of up 10 codes per request.
This API is not atomic and does not follow the typical guidelines for a batch API due to the sending of SMS messages.
The server attempts to issue every code in the batch. If errors are encountered, each item in codes
will contain the error details for
the corresponding code. The overall request will get the error status code of the first seen error, although some codes may have
succeeded.
eg.
{
codes: [
{
"error": "the first code failed",
"errorCode": "missing_date",
},
{
"uuid": "string UUID",
"code": "short verification code",
"expiresAt": "RFC1123 formatted string timestamp",
"expiresAtTimestamp": 0,
"expiresAt": "RFC1123 UTC timestamp",
"expiresAtTimestamp": 0,
},
{
"error": "the third code failed",
"errorCode": "unparsable_request",
},
],
error: "the first code failed",
errorCode: "missing_date",
}
``
**BatchIssueCodeRequest**
```json
{
"codes" : [
{
"symptomDate": "YYYY-MM-DD",
"testDate": "YYYY-MM-DD",
"testType": "<valid test type>",
"tzOffset": 0,
"phone": "+CC Phone number",
"padding": "<bytes>",
"uuid": "optional string UUID",
"externalIssuerID": "external-ID",
},
{
...
},
]
}
BatchIssueCodeResponse
Note: The error
and errorCode
of the outer response body will match the first error from the codes
array.
The response http code will also match the first seen error. The caller should iterate codes
to handle errors
for each code response. The index of each responses will match the index of the original request.
{
"codes": [
{
"uuid": "string UUID",
"code": "short verification code",
"expiresAt": "RFC1123 formatted string timestamp",
"expiresAtTimestamp": 0,
"expiresAt": "RFC1123 UTC timestamp",
"expiresAtTimestamp": 0,
"longExpiresAt": "RFC1123 UTC timestamp",
"longExpiresAtTimestamp": 0,
"error": "descriptive error message",
"errorCode": "well defined error code from api.go",
},
{
...
},
],
"error": "descriptive error message",
"errorCode": "well defined error code from api.go",
}
Checks the status of a previous issued code, looking up by UUID.
CheckCodeStatusRequest
{
"uuid": "UUID for code to check",
"padding": "<bytes>"
}
padding
is a recommended field that obfuscates the size of the request body to a network observer. The client should generate and insert a random number of base64-encoded bytes into this field. The server does not process the padding.
CheckCodeStatusResponse
{
"claimed": false,
"expiresAtTimestamp": 0,
"longExpiresAtTimestamp": 0,
"error": "descriptive error message",
"errorCode": "well defined error code from api.go",
"padding": "<bytes>"
}
claimed
- boolean indicating if the code was used or not
expiresAtTimestamp
- seconds since the epoch indicating expiry time in UTC
longExpiresAtTimestamp
- seconds since the epoch for the SMS link expiry time in UTC
padding
is a field that obfuscates the size of the response body to a network observer. The server may generate and insert a random number of base64-encoded bytes into this field. The client should not process the padding.
Expires an unclaimed code. If the code has been claimed an error is returned.
ExpireCodeRequest
{
"uuid": "UUID of the code to expire",
"expiresAtTimestamp": 0,
"longExpiresAtTimestamp": 0,
"error": "descriptive error message",
"errorCode": "well defined error code from api.go",
"padding": "<bytes>"
}
padding
is a recommended field that obfuscates the size of the request body to a network observer. The client should generate and insert a random number of base64-encoded bytes into this field. The server does not process the padding.
The timestamps are updated to the new expiration time (which will be in the past).
The statistics API are currently in preview. They are not covered by our backwards-compatibility promise and the APIs are subject to change without notice!
This path includes realm-level statistics for the past 30 days.
-
/api/stats/realm.{csv,json}
- Daily statistics for the realm, including codes issued, codes claimed, and daily active users (if enabled). -
/api/stats/realm-user.{csv,json}
- Daily statistics for codes issued by realm user. These statistics only include codes issued by humans logged into the verification system. -
/api/stats/realm-external-issuser.{csv,json}
- Daily statistics for codes issued by external issuers. These statistics only include codes issued by the API where anexternalIssuer
field was provided.
In addition to "real" requests, the server also accepts chaff (fake) requests. These can be used to obfuscate real traffic from a network observer or server operator.
Chaff requests:
- MUST send the
X-API-Key
header with a valid API key (otherwise you will get an unauthorized error) - MUST be sent via a
POST
request, otherwise you will get an invalid method error - SHOULD send a valid JSON body with random padding similar in size as the rest of the client requests so that chaff requests appear the same on the wire as other valid requests.
To initiate a chaff request, set the X-Chaff
header on your request:
curl https://apiserver.example.com/api/verify \
--header "x-api-key: YOUR-API-KEY" \
--header "content-type: application/json" \
--header "accept: application/json" \
--header "x-chaff: 1" \
--request POST \
--data '{"padding":"base64 encoded padding"}'
If the X-Chaff
header has a value of "daily" (case-insensitive), the request
will be counted toward the realm's daily active user count. Since the server
records data in UTC time, clients should send this value once per UTC day if
they are collecting server-side adoption metrics. The server will still respond
with fake data that should be ignored per guidance below.
The client should still send a real request with a real request body (the body will not be processed). The server will respond with a fake response that your client MUST NOT process or parse. The response will not be a valid JSON object.
Client's should sporadically issue chaff requests to mirror real-world usage.
You can expect the following responses from this API:
-
400
- The client made a bad/invalid request. Search the JSON response body for the"errors"
key. The body may be empty. -
401
- The client is unauthorized. This could be an invalid API key or revoked permissions. This usually has no"errors"
key, but clients can try to read the JSON body to see if there's additional information (it may be empty) -
404
- The client made a request to an invalid URL (routing error). Do not retry. -
405
- The client used the wrong HTTP verb. Do not retry. -
412
- The client requested a precondition that cannot be satisfied. -
429
- The client is rate limited. Check theRetry-After
header to determine when to retry the request. Clients can also monitor theX-RateLimit-Remaining
header that's returned with all responses to determine their rate limit and rate limit expiration. -
5xx
- Internal server error. Clients should retry with a reasonable backoff algorithm and maximum cap.