KYC
Upload KYC documents
Section titled “Upload KYC documents”Request
POST https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kyc/documentsAuthorization: Bearer <access-token>Accept: application/jsonContent-Type: multipart/form-data; boundary=kyc-boundary--kyc-boundaryContent-Disposition: form-data; name="manifest"Content-Type: application/json
[{"reference":"cac-0001","type":"CAC_CERTIFICATE"},{"reference":"bill-0001","type":"UTILITY_BILL"}]--kyc-boundaryContent-Disposition: form-data; name="cac-0001"; filename="cac-certificate.pdf"Content-Type: application/pdf
<file bytes>--kyc-boundaryContent-Disposition: form-data; name="bill-0001"; filename="utility-bill.jpg"Content-Type: image/jpeg
<file bytes>--kyc-boundary--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
| Field | Type | Required | Description |
|---|---|---|---|
| customerId (path) | string, UUID | Yes | The customer, from opening the account. |
| manifest (part) | JSON array | Yes | One entry per file. |
| manifest[].reference | string | Yes | Your reference for the file. Names the file’s part. |
| manifest[].type | enum: CAC_CERTIFICATE, STATUS_REPORT, FORM_BN1, FORM_CAC11, MEMART, BOARD_RESOLUTION, UTILITY_BILL, GOVERNMENT_ID, SELFIE, PREMISES_PHOTO, TAX_CLEARANCE, EVIDENCE_OF_FUNDS, AGENCY_TERMINATION | Yes | The document type. |
<reference> (part) | file: PDF, JPEG or PNG, max 10 MB | Yes, one per manifest entry | The 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
| Field | Type | Description |
|---|---|---|
| documents[].reference | string | Your reference for the file. |
| documents[].documentId | string, UUID | The document’s id. |
| documents[].type | enum: CAC_CERTIFICATE, STATUS_REPORT, FORM_BN1, FORM_CAC11, MEMART, BOARD_RESOLUTION, UTILITY_BILL, GOVERNMENT_ID, SELFIE, PREMISES_PHOTO, TAX_CLEARANCE, EVIDENCE_OF_FUNDS, AGENCY_TERMINATION | The document type. |
| documents[].contentType | enum: application/pdf, image/jpeg, image/png | The file type, detected from the file’s content. |
| documents[].size | integer, bytes | The file’s size. |
| documents[].sha256 | string, 64 hex characters | The SHA-256 of the file. |
| documents[].receivedAt | timestamp | When the file was received. |
- The file type is detected from the file’s content; the part’s
Content-Typeis 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
referencesent again with the same file returns the original document; with a different file it returns 409, code94. - Refer to a document in the pack by exactly one of
{"reference": "..."}or{"documentId": "..."}. - Allowed before submission and after a rejection.
Errors
| HTTP status | Body |
|---|---|
| 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"}} |
Submit the KYC pack
Section titled “Submit the KYC pack”Request
PUT https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kycAuthorization: Bearer <access-token>Accept: application/jsonContent-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 -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
| Field | Type | Required | Description |
|---|---|---|---|
| customerId (path) | string, UUID | Yes | The customer, from opening the account. |
| business.registrationType | enum: BN, RC | Yes | The customer’s own CAC registration type. |
| business.registrationNumber | string | Yes | The customer’s own CAC registration number. |
| business.certificate | document | Yes | The CAC certificate. |
| business.statusReport | document | Yes | A STATUS_REPORT or FORM_BN1 document for BN; a STATUS_REPORT or FORM_CAC11 document for RC. |
| business.proofOfAddress | document | Yes | The business’s proof of address. |
| business.memart | document | Yes for RC; refused for BN | The memorandum and articles of association. |
| business.boardResolution | document | Yes for RC; refused for BN | The board resolution. |
| people | array, at least 1 entry | Yes | Everyone on the CAC record. |
| people[].role | enum: PROPRIETOR, PARTNER for BN; DIRECTOR, SHAREHOLDER for RC | Yes | The person’s role on the CAC record. |
| people[].particulars.firstName | string | Yes | First name. |
| people[].particulars.middleName | string | No | Middle name. |
| people[].particulars.lastName | string | Yes | Last name. |
| people[].particulars.dateOfBirth | date, YYYY-MM-DD | Yes | Date of birth. |
| people[].particulars.phoneNumber | string | No | Phone number. |
| people[].particulars.email | string, email address | No | Email address. |
| people[].bvn | string, 11 digits | Yes | The person’s BVN. |
| people[].nin | string, 11 digits | Yes | The person’s NIN. |
| people[].governmentId | document | Yes | The person’s government ID. |
| people[].selfie | document | Yes | A selfie of the person. |
| employees | array, at least 1 entry | Yes | The business’s employees. |
| employees[].particulars.firstName | string | Yes | First name. |
| employees[].particulars.middleName | string | No | Middle name. |
| employees[].particulars.lastName | string | Yes | Last name. |
| employees[].particulars.dateOfBirth | date, YYYY-MM-DD | Yes | Date of birth. |
| employees[].particulars.phoneNumber | string | No | Phone number. |
| employees[].particulars.email | string, email address | No | Email address. |
| employees[].bvn | string, 11 digits | Yes | The employee’s BVN. |
| employees[].nin | string, 11 digits | Yes | The employee’s NIN. |
| signatories | array, at least 1 entry | Yes | The account’s signatories. |
| signatories[].particulars.firstName | string | Yes | First name. |
| signatories[].particulars.middleName | string | No | Middle name. |
| signatories[].particulars.lastName | string | Yes | Last name. |
| signatories[].particulars.dateOfBirth | date, YYYY-MM-DD | Yes | Date of birth. |
| signatories[].particulars.phoneNumber | string | No | Phone number. |
| signatories[].particulars.email | string, email address | No | Email address. |
| signatories[].bvn | string, 11 digits | Yes | The signatory’s BVN. |
| premises.address.line | string | Yes | The premises’ street address. |
| premises.address.city | string | Yes | The city. |
| premises.address.state | string | Yes | The state. |
| premises.location.lat | number | Yes | The premises’ latitude. |
| premises.location.lng | number | Yes | The premises’ longitude. |
| premises.photo | document | Yes | A photo of the premises. |
| tax.tin | string | Yes | The business’s tax identification number. |
| tax.clearance | document | Yes | One tax clearance certificate, which covers the three preceding years. |
| funds.evidence | document | Yes | Evidence of funds. |
| previousAgencies | array | No | Agency relationships the business has had. |
| previousAgencies[].principal | string | Yes | The principal the business was an agent for. |
| previousAgencies[].ended | boolean | Yes | Whether the agency has ended. |
| previousAgencies[].evidence | document | Yes when ended is true | Evidence 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
| Field | Type | Description |
|---|---|---|
| customerId | string, UUID | The customer. |
| status | enum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVED | The KYC status: IN_REVIEW once submitted. |
| tier | integer, 1 to 3 | The customer’s KYC tier. |
| submittedAt | timestamp | When the pack was submitted. |
| decidedAt | timestamp or null | When Spay decided; null until then. |
| items[].path | string | The item’s path in the pack. Items are sorted by path. |
| items[].status | enum: MISSING, RECEIVED, ACCEPTED, REJECTED | The item’s status. |
| items[].reason.code | enum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHER | Why the item was rejected. reason is present only on REJECTED. |
| items[].reason.note | string | The 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, reasonIDENTITY_NOT_VERIFIED, and the failing paths infields; nothing is recorded. If verification is unavailable, the pack is accepted and the check completes later. fields[].messagefor an identity refusal is one ofBVN 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: apackpart (this JSON), amanifestpart and the file parts, as in Upload KYC documents. Errorfieldsthen point into the pack (people[0].bvn) or the manifest (manifest[0]), and the upload errors (409 code94, 413, 415) apply. - Allowed once, before the first submission. After a rejection, use Resubmit rejected KYC items.
Errors
| HTTP status | Body |
|---|---|
| 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"}} |
Get KYC status
Section titled “Get KYC status”Request
GET https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kycAuthorization: Bearer <access-token>Accept: application/jsoncurl '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
| Field | Type | Required | Description |
|---|---|---|---|
| customerId (path) | string, UUID | Yes | The 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
| Field | Type | Description |
|---|---|---|
| customerId | string, UUID | The customer. |
| status | enum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVED | The KYC status. |
| tier | integer, 1 to 3 | The customer’s KYC tier. |
| submittedAt | timestamp or null | When the pack was submitted; null before submission. |
| decidedAt | timestamp or null | When Spay decided; null until then. |
| items[].path | string | The item’s path in the pack. |
| items[].status | enum: MISSING, RECEIVED, ACCEPTED, REJECTED | The item’s status. |
| items[].reason.code | enum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHER | Why the item was rejected. reason is present only on REJECTED. |
| items[].reason.note | string | The reviewer’s text. |
- Before submission, every required item is listed as
MISSING:business.certificate,business.statusReport,business.proofOfAddress(andbusiness.memart,business.boardResolutionforRC),people,employees,signatories,premises.address,premises.location,premises.photo,tax.tin,tax.clearance,funds.evidence.
Errors
| HTTP status | Body |
|---|---|
| 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"}} |
Resubmit rejected KYC items
Section titled “Resubmit rejected KYC items”Request
PATCH https://api-dev.spay.ng/api/v1/customers/22222222-2222-4222-8222-222222222222/kycAuthorization: Bearer <access-token>Accept: application/jsonContent-Type: application/json{ "items": [ { "path": "business.proofOfAddress", "value": { "reference": "bill-0002" } }, { "path": "people[0].nin", "value": "98765432109" } ]}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
| Field | Type | Required | Description |
|---|---|---|---|
| customerId (path) | string, UUID | Yes | The customer, from opening the account. |
| items | array | Yes | Every REJECTED path, and only those. |
| items[].path | string | Yes | A path the rejection named. |
| items[].value | string, document, object or array, as at path in Submit the KYC pack | Yes | The 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
| Field | Type | Description |
|---|---|---|
| customerId | string, UUID | The customer. |
| status | enum: PENDING_SUBMISSION, IN_REVIEW, REJECTED, APPROVED | The KYC status: IN_REVIEW once resubmitted. |
| tier | integer, 1 to 3 | The customer’s KYC tier. |
| submittedAt | timestamp | When the pack was submitted. |
| decidedAt | timestamp or null | When Spay decided. |
| items[].path | string | The item’s path in the pack. |
| items[].status | enum: MISSING, RECEIVED, ACCEPTED, REJECTED | The item’s status. |
| items[].reason.code | enum: ILLEGIBLE, EXPIRED, MISMATCH, MISSING, NOT_ACCEPTED, OTHER | Why the item was rejected. reason is present only on REJECTED. |
| items[].reason.note | string | The reviewer’s text. |
- Can also be sent as
multipart/form-datawith apackpart (this JSON), amanifestpart and the file parts; the upload errors (409 code94, 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
statusisREJECTED.
Errors
| HTTP status | Body |
|---|---|
| 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"}} |