Skip to content

KYC

Request

POST https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc/documents
Authorization: Bearer <access-token>
Accept: application/json
Content-Type: multipart/form-data; boundary=kyc-boundary
--kyc-boundary
Content-Disposition: form-data; name="manifest"
Content-Type: application/json
[{"reference":"cac-0001","type":"CAC_CERTIFICATE"},{"reference":"bill-0001","type":"UTILITY_BILL"}]
--kyc-boundary
Content-Disposition: form-data; name="cac-0001"; filename="cac-certificate.pdf"
Content-Type: application/pdf
<file bytes>
--kyc-boundary
Content-Disposition: form-data; name="bill-0001"; filename="utility-bill.jpg"
Content-Type: image/jpeg
<file bytes>
--kyc-boundary--
curl
curl -X POST 'https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc/documents' \
-H "Authorization: Bearer $SPAY_ACCESS_TOKEN" \
-H "Accept: application/json" \
-F 'manifest=[{"reference":"cac-0001","type":"CAC_CERTIFICATE"},{"reference":"bill-0001","type":"UTILITY_BILL"}];type=application/json' \
-F 'cac-0001=@cac-certificate.pdf;type=application/pdf' \
-F 'bill-0001=@utility-bill.jpg;type=image/jpeg'

Request fields

FieldTypeRequiredDescription
customerId (path)string, UUIDYesThe customer, from opening the account.
manifest (part)JSON arrayYesOne entry per file.
manifest[].referencestringYesYour reference for the file. Names the file’s part.
manifest[].typeenum: CAC_CERTIFICATE, STATUS_REPORT, FORM_BN1, FORM_CAC11, MEMART, BOARD_RESOLUTION, UTILITY_BILL, GOVERNMENT_ID, SELFIE, PREMISES_PHOTO, TAX_CLEARANCE, EVIDENCE_OF_FUNDS, AGENCY_TERMINATIONYesThe document type.
<reference> (part)file: PDF, JPEG or PNG, max 10 MBYes, one per manifest entryThe file, in a part named by its reference.

Response: 201 Created

{
"documents": [
{
"reference": "cac-0001",
"documentId": "66666666-6666-4666-8666-666666666666",
"type": "CAC_CERTIFICATE",
"contentType": "application/pdf",
"size": 182344,
"sha256": "9f2c4e0b7a1d3c5e8f6a2b4d6c8e0f1a3b5d7c9e1f2a4b6c8d0e2f4a6b8c0d2e",
"receivedAt": "2026-10-28T20:28:52.989694+01:00"
},
{
"reference": "bill-0001",
"documentId": "77777777-7777-4777-8777-777777777777",
"type": "UTILITY_BILL",
"contentType": "image/jpeg",
"size": 90211,
"sha256": "1b3d5f7a9c2e4a6c8e0b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c",
"receivedAt": "2026-10-28T20:28:52.989694+01:00"
}
]
}

Response fields

FieldTypeDescription
documents[].referencestringYour reference for the file.
documents[].documentIdstring, UUIDThe document’s id.
documents[].typeenum: CAC_CERTIFICATE, STATUS_REPORT, FORM_BN1, FORM_CAC11, MEMART, BOARD_RESOLUTION, UTILITY_BILL, GOVERNMENT_ID, SELFIE, PREMISES_PHOTO, TAX_CLEARANCE, EVIDENCE_OF_FUNDS, AGENCY_TERMINATIONThe document type.
documents[].contentTypeenum: application/pdf, image/jpeg, image/pngThe file type, detected from the file’s content.
documents[].sizeinteger, bytesThe file’s size.
documents[].sha256string, 64 hex charactersThe SHA-256 of the file.
documents[].receivedAttimestampWhen the file was received.
  • The file type is detected from the file’s content; the part’s Content-Type is ignored. At most 25 files and 60 MB per request.
  • All or nothing: if any file is refused, nothing in the request is kept.
  • A reference sent again with the same file returns the original document; with a different file it returns 409, code 94.
  • Refer to a document in the pack by exactly one of {"reference": "..."} or {"documentId": "..."}.
  • Allowed before submission and after a rejection.

Errors

HTTP statusBody
400{"error":{"code":"30","message":"Invalid request","fields":[{"field":"manifest[0]","message":"File must be a PDF, JPEG or PNG"}]}}
401{"error":{"code":"63","message":"Missing, invalid or expired access token"}}
403{"error":{"code":"63","message":"Operation not permitted"}}
404{"error":{"code":"25","message":"Resource not found"}}
409{"error":{"code":"94","message":"Reference has already been used; query the existing operation","fields":[{"field":"manifest[1]","message":"Reference already used for another file"}]}}
409{"error":{"code":"12","message":"Operation not allowed in the current state"}}
413{"error":{"code":"30","message":"Invalid request"}}
415{"error":{"code":"30","message":"Invalid request"}}
503{"error":{"code":"91","message":"Service unavailable"}}

Request

PUT https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc
Authorization: Bearer <access-token>
Accept: application/json
Content-Type: application/json
{
"business": {
"registrationType": "BN",
"registrationNumber": "BN1234567",
"certificate": { "reference": "cac-0001" },
"statusReport": { "reference": "bn1-0001" },
"proofOfAddress": { "reference": "bill-0001" }
},
"people": [
{
"role": "PROPRIETOR",
"particulars": {
"firstName": "Ada",
"lastName": "Obi",
"dateOfBirth": "1990-01-15",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
},
"bvn": "12345678901",
"nin": "98765432109",
"governmentId": { "reference": "id-0001" },
"selfie": { "reference": "selfie-0001" }
}
],
"employees": [
{
"particulars": {
"firstName": "Tunde",
"lastName": "Bello",
"dateOfBirth": "1995-03-02"
},
"bvn": "22345678901",
"nin": "88765432109"
}
],
"signatories": [
{
"particulars": {
"firstName": "Ada",
"lastName": "Obi",
"dateOfBirth": "1990-01-15"
},
"bvn": "12345678901"
}
],
"premises": {
"address": { "line": "12 Market Road", "city": "Kano", "state": "Kano" },
"location": { "lat": 12.0022, "lng": 8.592 },
"photo": { "reference": "shop-0001" }
},
"tax": {
"tin": "12345678-0001",
"clearance": { "reference": "tcc-0001" }
},
"funds": { "evidence": { "reference": "funds-0001" } },
"previousAgencies": []
}
curl
curl -X PUT 'https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc' \
-H "Authorization: Bearer $SPAY_ACCESS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"business": {
"registrationType": "BN",
"registrationNumber": "BN1234567",
"certificate": { "reference": "cac-0001" },
"statusReport": { "reference": "bn1-0001" },
"proofOfAddress": { "reference": "bill-0001" }
},
"people": [
{
"role": "PROPRIETOR",
"particulars": {
"firstName": "Ada",
"lastName": "Obi",
"dateOfBirth": "1990-01-15",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
},
"bvn": "12345678901",
"nin": "98765432109",
"governmentId": { "reference": "id-0001" },
"selfie": { "reference": "selfie-0001" }
}
],
"employees": [
{
"particulars": {
"firstName": "Tunde",
"lastName": "Bello",
"dateOfBirth": "1995-03-02"
},
"bvn": "22345678901",
"nin": "88765432109"
}
],
"signatories": [
{
"particulars": {
"firstName": "Ada",
"lastName": "Obi",
"dateOfBirth": "1990-01-15"
},
"bvn": "12345678901"
}
],
"premises": {
"address": { "line": "12 Market Road", "city": "Kano", "state": "Kano" },
"location": { "lat": 12.0022, "lng": 8.592 },
"photo": { "reference": "shop-0001" }
},
"tax": {
"tin": "12345678-0001",
"clearance": { "reference": "tcc-0001" }
},
"funds": { "evidence": { "reference": "funds-0001" } },
"previousAgencies": []
}'

Request fields

FieldTypeRequiredDescription
customerId (path)string, UUIDYesThe customer, from opening the account.
business.registrationTypeenum: BN, RCYesThe customer’s own CAC registration type.
business.registrationNumberstringYesThe customer’s own CAC registration number.
business.certificatedocumentYesThe CAC certificate.
business.statusReportdocumentYesA STATUS_REPORT or FORM_BN1 document for BN; a STATUS_REPORT or FORM_CAC11 document for RC.
business.proofOfAddressdocumentYesThe business’s proof of address.
business.memartdocumentYes for RC; refused for BNThe memorandum and articles of association.
business.boardResolutiondocumentYes for RC; refused for BNThe board resolution.
peoplearray, at least 1 entryYesEveryone on the CAC record.
people[].roleenum: PROPRIETOR, PARTNER for BN; DIRECTOR, SHAREHOLDER for RCYesThe person’s role on the CAC record.
people[].particulars.firstNamestringYesFirst name.
people[].particulars.middleNamestringNoMiddle name.
people[].particulars.lastNamestringYesLast name.
people[].particulars.dateOfBirthdate, YYYY-MM-DDYesDate of birth.
people[].particulars.phoneNumberstringNoPhone number.
people[].particulars.emailstring, email addressNoEmail address.
people[].bvnstring, 11 digitsYesThe person’s BVN.
people[].ninstring, 11 digitsYesThe person’s NIN.
people[].governmentIddocumentYesThe person’s government ID.
people[].selfiedocumentYesA selfie of the person.
employeesarray, at least 1 entryYesThe business’s employees.
employees[].particulars.firstNamestringYesFirst name.
employees[].particulars.middleNamestringNoMiddle name.
employees[].particulars.lastNamestringYesLast name.
employees[].particulars.dateOfBirthdate, YYYY-MM-DDYesDate of birth.
employees[].particulars.phoneNumberstringNoPhone number.
employees[].particulars.emailstring, email addressNoEmail address.
employees[].bvnstring, 11 digitsYesThe employee’s BVN.
employees[].ninstring, 11 digitsYesThe employee’s NIN.
signatoriesarray, at least 1 entryYesThe account’s signatories.
signatories[].particulars.firstNamestringYesFirst name.
signatories[].particulars.middleNamestringNoMiddle name.
signatories[].particulars.lastNamestringYesLast name.
signatories[].particulars.dateOfBirthdate, YYYY-MM-DDYesDate of birth.
signatories[].particulars.phoneNumberstringNoPhone number.
signatories[].particulars.emailstring, email addressNoEmail address.
signatories[].bvnstring, 11 digitsYesThe signatory’s BVN.
premises.address.linestringYesThe premises’ street address.
premises.address.citystringYesThe city.
premises.address.statestringYesThe state.
premises.location.latnumberYesThe premises’ latitude.
premises.location.lngnumberYesThe premises’ longitude.
premises.photodocumentYesA photo of the premises.
tax.tinstringYesThe business’s tax identification number.
tax.clearancedocumentYesOne tax clearance certificate, which covers the three preceding years.
funds.evidencedocumentYesEvidence of funds.
previousAgenciesarrayNoAgency relationships the business has had.
previousAgencies[].principalstringYesThe principal the business was an agent for.
previousAgencies[].endedbooleanYesWhether the agency has ended.
previousAgencies[].evidencedocumentYes when ended is trueEvidence the agency ended.

Response: 202 Accepted

{
"customerId": "22222222-2222-4222-8222-222222222222",
"status": "IN_REVIEW",
"tier": 1,
"submittedAt": "2026-10-28T20:28:53.97479+01:00",
"decidedAt": null,
"items": [
{ "path": "business.certificate", "status": "RECEIVED" },
{ "path": "business.proofOfAddress", "status": "RECEIVED" },
{ "path": "business.statusReport", "status": "RECEIVED" },
{ "path": "employees[0].bvn", "status": "RECEIVED" },
{ "path": "employees[0].nin", "status": "RECEIVED" },
{ "path": "employees[0].particulars", "status": "RECEIVED" },
{ "path": "funds.evidence", "status": "RECEIVED" },
{ "path": "people[0].bvn", "status": "RECEIVED" },
{ "path": "people[0].governmentId", "status": "RECEIVED" },
{ "path": "people[0].nin", "status": "RECEIVED" },
{ "path": "people[0].particulars", "status": "RECEIVED" },
{ "path": "people[0].selfie", "status": "RECEIVED" },
{ "path": "premises.address", "status": "RECEIVED" },
{ "path": "premises.location", "status": "RECEIVED" },
{ "path": "premises.photo", "status": "RECEIVED" },
{ "path": "signatories[0].bvn", "status": "RECEIVED" },
{ "path": "signatories[0].particulars", "status": "RECEIVED" },
{ "path": "tax.clearance", "status": "RECEIVED" },
{ "path": "tax.tin", "status": "RECEIVED" }
]
}

Response fields

FieldTypeDescription
customerIdstring, UUIDThe customer.
statusenum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVEDThe KYC status: IN_REVIEW once submitted.
tierinteger, 1 to 3The customer’s KYC tier.
submittedAttimestampWhen the pack was submitted.
decidedAttimestamp or nullWhen Spay decided; null until then.
items[].pathstringThe item’s path in the pack. Items are sorted by path.
items[].statusenum: MISSING, RECEIVED, ACCEPTED, REJECTEDThe item’s status.
items[].reason.codeenum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHERWhy the item was rejected. reason is present only on REJECTED.
items[].reason.notestringThe reviewer’s text.
  • A document field is {"reference": "..."} or {"documentId": "..."}, naming a file from Upload KYC documents.
  • The owner may also be listed as an employee and a signatory.
  • Each BVN and NIN is verified during the call. One that isn’t found or doesn’t match refuses the submission with code 30, reason IDENTITY_NOT_VERIFIED, and the failing paths in fields; nothing is recorded. If verification is unavailable, the pack is accepted and the check completes later.
  • fields[].message for an identity refusal is one of BVN not found, NIN not found, The name does not match the BVN, The name does not match the NIN, The date of birth does not match the BVN, The date of birth does not match the NIN.
  • The pack and its files can also be sent in one request as multipart/form-data: a pack part (this JSON), a manifest part and the file parts, as in Upload KYC documents. Error fields then point into the pack (people[0].bvn) or the manifest (manifest[0]), and the upload errors (409 code 94, 413, 415) apply.
  • Allowed once, before the first submission. After a rejection, use Resubmit rejected KYC items.

Errors

HTTP statusBody
400{"error":{"code":"30","message":"Invalid request","fields":[{"field":"people[0].particulars.firstName","message":"Required"}]}}
400{"error":{"code":"30","message":"Identity not verified","reason":"IDENTITY_NOT_VERIFIED","fields":[{"field":"people[0].bvn","message":"BVN not found"},{"field":"employees[0].nin","message":"The name does not match the NIN"}]}}
401{"error":{"code":"63","message":"Missing, invalid or expired access token"}}
403{"error":{"code":"63","message":"Operation not permitted"}}
404{"error":{"code":"25","message":"Resource not found"}}
409{"error":{"code":"12","message":"Operation not allowed in the current state"}}
413{"error":{"code":"30","message":"Invalid request"}}
415{"error":{"code":"30","message":"Invalid request"}}
503{"error":{"code":"91","message":"Service unavailable"}}

Request

GET https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc
Authorization: Bearer <access-token>
Accept: application/json
curl
curl 'https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc' \
-H "Authorization: Bearer $SPAY_ACCESS_TOKEN" \
-H "Accept: application/json"

No request body.

Request fields

FieldTypeRequiredDescription
customerId (path)string, UUIDYesThe customer, from opening the account.

Response: 200 OK

{
"customerId": "22222222-2222-4222-8222-222222222222",
"status": "REJECTED",
"tier": 1,
"submittedAt": "2026-10-28T10:15:00+01:00",
"decidedAt": "2026-10-29T14:00:00+01:00",
"items": [
{ "path": "business.certificate", "status": "ACCEPTED" },
{
"path": "business.proofOfAddress",
"status": "REJECTED",
"reason": { "code": "EXPIRED", "note": "Bill is older than 3 months" }
},
{ "path": "people[0].bvn", "status": "ACCEPTED" },
{
"path": "people[0].nin",
"status": "REJECTED",
"reason": { "code": "MISMATCH", "note": "NIN belongs to another person" }
}
]
}

Response fields

FieldTypeDescription
customerIdstring, UUIDThe customer.
statusenum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVEDThe KYC status.
tierinteger, 1 to 3The customer’s KYC tier.
submittedAttimestamp or nullWhen the pack was submitted; null before submission.
decidedAttimestamp or nullWhen Spay decided; null until then.
items[].pathstringThe item’s path in the pack.
items[].statusenum: MISSING, RECEIVED, ACCEPTED, REJECTEDThe item’s status.
items[].reason.codeenum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHERWhy the item was rejected. reason is present only on REJECTED.
items[].reason.notestringThe reviewer’s text.
  • Before submission, every required item is listed as MISSING: business.certificate, business.statusReport, business.proofOfAddress (and business.memart, business.boardResolution for RC), people, employees, signatories, premises.address, premises.location, premises.photo, tax.tin, tax.clearance, funds.evidence.

Errors

HTTP statusBody
400{"error":{"code":"30","message":"Invalid request"}}
401{"error":{"code":"63","message":"Missing, invalid or expired access token"}}
403{"error":{"code":"63","message":"Operation not permitted"}}
404{"error":{"code":"25","message":"Resource not found"}}
503{"error":{"code":"91","message":"Service unavailable"}}

Request

PATCH https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc
Authorization: Bearer <access-token>
Accept: application/json
Content-Type: application/json
{
"items": [
{ "path": "business.proofOfAddress", "value": { "reference": "bill-0002" } },
{ "path": "people[0].nin", "value": "98765432109" }
]
}
curl
curl -X PATCH 'https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc' \
-H "Authorization: Bearer $SPAY_ACCESS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "path": "business.proofOfAddress", "value": { "reference": "bill-0002" } },
{ "path": "people[0].nin", "value": "98765432109" }
]
}'

Request fields

FieldTypeRequiredDescription
customerId (path)string, UUIDYesThe customer, from opening the account.
itemsarrayYesEvery REJECTED path, and only those.
items[].pathstringYesA path the rejection named.
items[].valuestring, document, object or array, as at path in Submit the KYC packYesThe new value. A rejected collection (people, employees, signatories, previousAgencies) is sent again in full as an array.

Response: 202 Accepted with the body of Get KYC status, status IN_REVIEW.

Response fields

FieldTypeDescription
customerIdstring, UUIDThe customer.
statusenum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVEDThe KYC status: IN_REVIEW once resubmitted.
tierinteger, 1 to 3The customer’s KYC tier.
submittedAttimestampWhen the pack was submitted.
decidedAttimestamp or nullWhen Spay decided.
items[].pathstringThe item’s path in the pack.
items[].statusenum: MISSING, RECEIVED, ACCEPTED, REJECTEDThe item’s status.
items[].reason.codeenum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHERWhy the item was rejected. reason is present only on REJECTED.
items[].reason.notestringThe reviewer’s text.
  • Can also be sent as multipart/form-data with a pack part (this JSON), a manifest part and the file parts; the upload errors (409 code 94, 413, 415) then apply.
  • A BVN or NIN sent again, or a person whose particulars are sent again, is verified as in Submit the KYC pack.
  • Allowed only while status is REJECTED.

Errors

HTTP statusBody
400{"error":{"code":"30","message":"Invalid request","fields":[{"field":"people[0].nin","message":"Must be sent again"}]}}
400{"error":{"code":"30","message":"Invalid request","fields":[{"field":"items[1].path","message":"Not named by the rejection"}]}}
400{"error":{"code":"30","message":"Identity not verified","reason":"IDENTITY_NOT_VERIFIED","fields":[{"field":"people[0].nin","message":"NIN not found"}]}}
401{"error":{"code":"63","message":"Missing, invalid or expired access token"}}
403{"error":{"code":"63","message":"Operation not permitted"}}
404{"error":{"code":"25","message":"Resource not found"}}
409{"error":{"code":"12","message":"Operation not allowed in the current state"}}
413{"error":{"code":"30","message":"Invalid request"}}
415{"error":{"code":"30","message":"Invalid request"}}
503{"error":{"code":"91","message":"Service unavailable"}}