KYC document rejected
The carrier reviewed a KYC document and refused it. kyc_document.rejection_reason
says why; send a replacement under the same documentType with
POST /numbers/orders/{id}/documents.
Delivery contract (all events):
- We send a
POSTwith a JSON body to thewebhookURL you gave onPOST /numbers/orders. The body is the order exactly asGET /numbers/orders/{id}returns it, plusevent,delivery_idand, on document events,kyc_document. The bodystatusis the order's status at the time of the event, not necessarily its current one. - Answer with any
2xxstatus to confirm. Anything else (or no answer within 10 seconds) is retried, up to 6 attempts in total (the first, then retries after 3 minutes, 10 minutes, 1 hour, 6 hours and 24 hours), then the delivery is marked failed (it can be retried from Cockpit). Redirects are not followed. - Delivery is at-least-once: dedupe on
X-Voxbi-Delivery. - Events can arrive out of order (a retry of an older event can land after a
newer one). Use
updated_atand the bodystatus, or fetchGET /numbers/orders/{id}, when order matters. - Verify
X-Voxbi-Signaturebefore trusting the body.
No authentication required
X-Voxbi-Event
header · string
*
The event this delivery is about, for example `order.fulfilled`. Same value as
`event` in the body. Use it to route the request before parsing the body.
enum:
order.fulfilled order.failed order.canceled kyc.document_accepted kyc.document_rejected kyc.validated
X-Voxbi-Delivery
header · string · uuid
*
Unique id of this delivery (a UUID). Every retry of the same delivery carries the
same value, so store it and ignore a delivery you have already processed:
delivery is at-least-once. Same value as `delivery_id` in the body.
X-Voxbi-Signature
header · string
*
Proves the request comes from Voxbi and the body was not altered. Format:
`t=<unix timestamp>,v1=<hex>[,v1=<hex>]`.
`<hex>` is the lowercase hex HMAC-SHA256 of the string `<t>.<raw body>` (the
timestamp, a dot, then the exact bytes of the request body) keyed with your
webhook signing secret (it starts with `whsec_`). Right after you rotate the
secret, the header carries two `v1` values for 24 hours (one per valid secret):
accept the request if ANY of them matches.
To verify:
1. Read the raw request body BEFORE parsing the JSON.
2. Split the header on `,`; take `t` and every `v1`.
3. Compute `HMAC_SHA256(secret, t + "." + raw_body)` as hex.
4. Compare it to each `v1` with a constant-time comparison. No match: reject with `401`.
5. Optionally reject when `t` is more than 5 minutes from your clock, to stop replays.
Pseudo-code: `expected = hex(hmac_sha256(secret, t + "." + raw_body))`.
The secret is managed in Cockpit under Settings, API keys and secrets, Webhook
signing secret, where you can reveal and rotate it.
Request body · required · application/json
Request schema
allOf
idstring · uuid
Internal order id assigned by us. Use this with `GET /numbers/orders/{id}`
to fetch the latest state, and `POST /numbers/orders/{id}/documents` to
send KYC documents. Also the `id` in every webhook payload (the webhook body
is this same object). Your own reference is `custom_order_reference`.
example:
550e8400-e29b-41d4-a716-446655440000order_idstring | null
DEPRECATED: same value as `custom_order_reference`, kept for existing
integrations. Read `custom_order_reference` instead.
length: 0–64deprecated
example:
po-2026-00042custom_order_referencestring | null
Your own reference for the order, supplied as `custom_order_reference` (or the
deprecated `order_id`) on `POST /numbers/orders`. Echoed back here, in
`GET /numbers/orders` (filterable by it) and in every webhook payload. If you
sent none, the carrier's generated one is backfilled here.
length: 0–64
example:
po-2026-00042order_referencestring | null · uuid
The carrier's own identifier for this order (a UUID they assign). Null
until the order is placed. This is the reference the carrier says to use
to identify the order, and it is distinct from `custom_order_reference` (your own
reference, opaque to the carrier). Not settable by you.
example:
01a0ae76-0000-4000-8000-000000000000customer_namestring
Legal name of the customer the numbers were ordered for, as supplied on
create (and editable on retry).
example:
ACME GmbHstatusstring
Lifecycle state of this order.
* `requested`: sent upstream, awaiting fulfilment.
* `submission_failed`: rejected during synchronous submission; editable + retryable.
* `processing`: async success callback received; provisioning in progress.
* `success`: provisioning complete; `numbers` is populated.
* `success_kyc_required`: the numbers are provisioned (`numbers` is populated)
but the carrier has not validated the KYC yet; see `kyc`. Moves to `success`
once the KYC is validated.
* `error`: async rejection; terminal.
* `processing_failed`: internal processing exhausted retries; admin-only retry.
* `canceled`: a reseller closed a `submission_failed` order that provably never
reached the carrier; terminal. Nothing was canceled at the carrier and no webhook
is sent.
enum:
created requested submission_failed processing success success_kyc_required error processing_failed canceledexample:
requestedaddressobject
streetstring
example:
Adenaueralleestreet_numberstring
example:
1postal_codestring
example:
53113citystring
example:
Bonnarea_codestring | null
Country-conditional (e.g. set for CH); null otherwise.
countrystring
example:
DEformatted_addressstring | null
Single-line, comma-separated address derived from the persisted
components (street + number, postal code + city, country). Empty
components are dropped.
example:
Adenauerallee 1, 53113 Bonn, DEquantityinteger
example:
1prefixstring | null
Dial prefix in E.164 (country + area code if applicable).
example:
+49228numbersarray<string>
E.164-formatted numbers assigned to this order. Empty (`[]`) until the
order reaches `success` - they are never returned synchronously by
`POST /numbers/orders` (which only ever returns `requested`); they arrive
later via the webhook and then appear here on `GET /numbers/orders/{id}`.
[]string
phone_numbersarray<object>
The assigned numbers, in the same shape as `GET /numbers`
(id, number, name, is_active, call_flow), so you can read each number's id
and current call flow and act on it directly (e.g. `PUT /numbers/{id}/call-flow`).
Populated once the order reaches `success`.
idstring · uuid
example:
550e8400-e29b-41d4-a716-446655440000numberstring
Phone number in E.164 format.
example:
+12125550100namestring | null
example:
Customer Supportis_activeboolean
example:
1call_flow_idstring | null · uuid
example:
550e8400-e29b-41d4-a716-446655440100call_flowobject | null
idstring · uuid
namestring
enabledboolean
error_messagestring | null
Short error reason when status is `error` or `submission_failed`.
kycobject
The order's KYC (know-your-customer) progress with the carrier. Present on every
order, including orders placed with KYC deferred (`kyc_required_at_order: false` on
`POST /numbers/orders/check`), where `missing` lists what is still to be sent via
`POST /numbers/orders/{id}/documents`.
statestring | null
Overall KYC state.
* `pending`: no verdict yet.
* `validated`: the carrier signed the KYC off (`validated_at` is set).
* `rejected`: not validated and at least one provided document is rejected.
Null until the carrier has reported on this order.
enum:
pending validated rejected nullexample:
pendingvalidated_atstring | null · date-time
When the carrier validated the KYC. Null until then.
providedarray<object>
Documents the carrier has on file, with their individual verdict.
Each item — One KYC document the carrier has on file for the order, with the carrier's
verdict on it.
document_typestring | null
The KYC document type code (the `documentType` you uploaded it under).
example:
utility_billstatusstring | null
The carrier's verdict on this document.
* `pending`: received, not reviewed yet.
* `accepted`: reviewed and accepted.
* `rejected`: reviewed and refused; see `rejection_reason` and upload a replacement.
* `unknown`: the carrier reported a status we do not recognize.
enum:
pending accepted rejected unknown nullexample:
acceptedrejection_reasonstring | null
Why the carrier refused the document. Null unless `status` is `rejected`.
created_atstring | null · date-time
When the carrier received the document (a replacement carries a new value).
example:
2026-05-12T11:15:03+00:00updated_atstring | null · date-time
When the carrier last changed the document (for example reviewed it).
example:
2026-05-13T08:02:41+00:00missingarray<object>
Documents still needed. Each entry is either an object (that exact document
is needed) or an array of objects (provide any ONE of them), like
`kycRequirements` on the check endpoint.
[]object
webhook_urlstring | null
The webhook URL we will POST the final outcome to.
webhook_statusstring
enum:
not_required pending retrying delivered failed cancelledwebhook_next_retry_atstring | null · date-time
created_atstring · date-time
updated_atstring · date-time
event*string
What happened. Same value as the `X-Voxbi-Event` header.
enum:
order.fulfilled order.failed order.canceled kyc.document_accepted kyc.document_rejected kyc.validatedexample:
order.fulfilleddelivery_id*string · uuid
Identifies this delivery; every retry of it carries the same value. Same
value as the `X-Voxbi-Delivery` header. Use it to ignore duplicates.
example:
7c9e6679-7425-40de-944b-e07fc1f90ae7kyc_documentobject
Only on `kyc.document_accepted` and `kyc.document_rejected`: the document
the carrier just decided on.
document_typestring
example:
utility_billstatusstring
enum:
accepted rejectedexample:
acceptedrejection_reasonstring | null
created_atstring | null · date-time
example:
2026-05-12T11:15:03+00:00updated_atstring | null · date-time
example:
2026-05-13T08:02:41+00:00Responses
Any `2xx` status confirms the delivery. Any other answer is retried on the
schedule above, then the delivery is marked failed. The response body is ignored.
Response schema
No response body.
post
kyc.document_rejected
Request sample
curl -X POST 'https://your-server.example.com/webhooks' \
-H 'Content-Type: application/json' \
--data '{"id":"550e8400-e29b-41d4-a716-446655440000","order_id":"po-2026-00042","custom_order_reference":"po-2026-00042","order_reference":"01a0ae76-0000-4000-8000-000000000000","customer_name":"ACME GmbH","status":"success_kyc_required","address":{"street":"Adenauerallee","street_number":"1","postal_code":"53113","city":"Bonn","area_code":null,"country":"DE"},"formatted_address":"Adenauerallee 1, 53113 Bonn, DE","quantity":1,"prefix":"+49228","numbers":["+4922812345678"],"phone_numbers":[{"id":"550e8400-e29b-41d4-a716-446655440001","number":"+4922812345678","name":null,"is_active":true,"call_flow_id":null,"call_flow":null}],"error_message":null,"kyc":{"state":"rejected","validated_at":null,"provided":[{"document_type":"utility_bill","status":"accepted","rejection_reason":null,"created_at":"2026-05-12T11:15:03+00:00","updated_at":"2026-05-13T08:02:41+00:00"},{"document_type":"id_document","status":"rejected","rejection_reason":"The photo is blurry. Please upload a sharper copy.","created_at":"2026-05-12T11:15:03+00:00","updated_at":"2026-05-13T08:02:41+00:00"}],"missing":[]},"webhook_url":"https://your-app.example/hooks/number-order","webhook_status":"pending","webhook_next_retry_at":null,"created_at":"2026-05-12T10:36:47+00:00","updated_at":"2026-05-13T08:02:41+00:00","event":"kyc.document_rejected","delivery_id":"c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f","kyc_document":{"document_type":"id_document","status":"rejected","rejection_reason":"The photo is blurry. Please upload a sharper copy.","created_at":"2026-05-12T11:15:03+00:00","updated_at":"2026-05-13T08:02:41+00:00"}}'
const response = await fetch('https://your-server.example.com/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "po-2026-00042",
"custom_order_reference": "po-2026-00042",
"order_reference": "01a0ae76-0000-4000-8000-000000000000",
"customer_name": "ACME GmbH",
"status": "success_kyc_required",
"address": {
"street": "Adenauerallee",
"street_number": "1",
"postal_code": "53113",
"city": "Bonn",
"area_code": null,
"country": "DE"
},
"formatted_address": "Adenauerallee 1, 53113 Bonn, DE",
"quantity": 1,
"prefix": "+49228",
"numbers": [
"+4922812345678"
],
"phone_numbers": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"number": "+4922812345678",
"name": null,
"is_active": true,
"call_flow_id": null,
"call_flow": null
}
],
"error_message": null,
"kyc": {
"state": "rejected",
"validated_at": null,
"provided": [
{
"document_type": "utility_bill",
"status": "accepted",
"rejection_reason": null,
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
},
{
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
],
"missing": []
},
"webhook_url": "https://your-app.example/hooks/number-order",
"webhook_status": "pending",
"webhook_next_retry_at": null,
"created_at": "2026-05-12T10:36:47+00:00",
"updated_at": "2026-05-13T08:02:41+00:00",
"event": "kyc.document_rejected",
"delivery_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
"kyc_document": {
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
}),
});
const data = await response.json();
console.log(data);
import requests
response = requests.post('https://your-server.example.com/webhooks',
json={
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "po-2026-00042",
"custom_order_reference": "po-2026-00042",
"order_reference": "01a0ae76-0000-4000-8000-000000000000",
"customer_name": "ACME GmbH",
"status": "success_kyc_required",
"address": {
"street": "Adenauerallee",
"street_number": "1",
"postal_code": "53113",
"city": "Bonn",
"area_code": null,
"country": "DE"
},
"formatted_address": "Adenauerallee 1, 53113 Bonn, DE",
"quantity": 1,
"prefix": "+49228",
"numbers": [
"+4922812345678"
],
"phone_numbers": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"number": "+4922812345678",
"name": null,
"is_active": true,
"call_flow_id": null,
"call_flow": null
}
],
"error_message": null,
"kyc": {
"state": "rejected",
"validated_at": null,
"provided": [
{
"document_type": "utility_bill",
"status": "accepted",
"rejection_reason": null,
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
},
{
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
],
"missing": []
},
"webhook_url": "https://your-app.example/hooks/number-order",
"webhook_status": "pending",
"webhook_next_retry_at": null,
"created_at": "2026-05-12T10:36:47+00:00",
"updated_at": "2026-05-13T08:02:41+00:00",
"event": "kyc.document_rejected",
"delivery_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
"kyc_document": {
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
}
)
response.raise_for_status()
data = response.json()
print(data)
<?php
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json",
'content' => '{
\"id\": \"550e8400-e29b-41d4-a716-446655440000\",
\"order_id\": \"po-2026-00042\",
\"custom_order_reference\": \"po-2026-00042\",
\"order_reference\": \"01a0ae76-0000-4000-8000-000000000000\",
\"customer_name\": \"ACME GmbH\",
\"status\": \"success_kyc_required\",
\"address\": {
\"street\": \"Adenauerallee\",
\"street_number\": \"1\",
\"postal_code\": \"53113\",
\"city\": \"Bonn\",
\"area_code\": null,
\"country\": \"DE\"
},
\"formatted_address\": \"Adenauerallee 1, 53113 Bonn, DE\",
\"quantity\": 1,
\"prefix\": \"+49228\",
\"numbers\": [
\"+4922812345678\"
],
\"phone_numbers\": [
{
\"id\": \"550e8400-e29b-41d4-a716-446655440001\",
\"number\": \"+4922812345678\",
\"name\": null,
\"is_active\": true,
\"call_flow_id\": null,
\"call_flow\": null
}
],
\"error_message\": null,
\"kyc\": {
\"state\": \"rejected\",
\"validated_at\": null,
\"provided\": [
{
\"document_type\": \"utility_bill\",
\"status\": \"accepted\",
\"rejection_reason\": null,
\"created_at\": \"2026-05-12T11:15:03+00:00\",
\"updated_at\": \"2026-05-13T08:02:41+00:00\"
},
{
\"document_type\": \"id_document\",
\"status\": \"rejected\",
\"rejection_reason\": \"The photo is blurry. Please upload a sharper copy.\",
\"created_at\": \"2026-05-12T11:15:03+00:00\",
\"updated_at\": \"2026-05-13T08:02:41+00:00\"
}
],
\"missing\": []
},
\"webhook_url\": \"https://your-app.example/hooks/number-order\",
\"webhook_status\": \"pending\",
\"webhook_next_retry_at\": null,
\"created_at\": \"2026-05-12T10:36:47+00:00\",
\"updated_at\": \"2026-05-13T08:02:41+00:00\",
\"event\": \"kyc.document_rejected\",
\"delivery_id\": \"c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f\",
\"kyc_document\": {
\"document_type\": \"id_document\",
\"status\": \"rejected\",
\"rejection_reason\": \"The photo is blurry. Please upload a sharper copy.\",
\"created_at\": \"2026-05-12T11:15:03+00:00\",
\"updated_at\": \"2026-05-13T08:02:41+00:00\"
}
}',
],
]);
$response = file_get_contents('https://your-server.example.com/webhooks', false, $context);
$data = json_decode($response, true);
print_r($data);
Sample request
{ … }
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "po-2026-00042",
"custom_order_reference": "po-2026-00042",
"order_reference": "01a0ae76-0000-4000-8000-000000000000",
"customer_name": "ACME GmbH",
"status": "success_kyc_required",
"address": { … },
"street": "Adenauerallee",
"street_number": "1",
"postal_code": "53113",
"city": "Bonn",
"area_code": null,
"country": "DE"
},
"formatted_address": "Adenauerallee 1, 53113 Bonn, DE",
"quantity": 1,
"prefix": "+49228",
"numbers": [ … ],
"+4922812345678"
],
"phone_numbers": [ … ],
{ … }
"id": "550e8400-e29b-41d4-a716-446655440001",
"number": "+4922812345678",
"name": null,
"is_active": true,
"call_flow_id": null,
"call_flow": null
}
],
"error_message": null,
"kyc": { … },
"state": "rejected",
"validated_at": null,
"provided": [ … ],
{ … },
"document_type": "utility_bill",
"status": "accepted",
"rejection_reason": null,
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
},
{ … }
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
],
"missing": []
},
"webhook_url": "https://your-app.example/hooks/number-order",
"webhook_status": "pending",
"webhook_next_retry_at": null,
"created_at": "2026-05-12T10:36:47+00:00",
"updated_at": "2026-05-13T08:02:41+00:00",
"event": "kyc.document_rejected",
"delivery_id": "c3d4e5f6-a7b8-4c9d-8e1f-2a3b4c5d6e7f",
"kyc_document": { … }
"document_type": "id_document",
"status": "rejected",
"rejection_reason": "The photo is blurry. Please upload a sharper copy.",
"created_at": "2026-05-12T11:15:03+00:00",
"updated_at": "2026-05-13T08:02:41+00:00"
}
}
No example for this status.