https://submission.herokuapp.com)/v1, /api/v1, or similarhttps://submission.herokuapp.com, we will test against POST https://submission.herokuapp.com/creator-cards, GET https://submission.herokuapp.com/creator-cards/:slug, and DELETE https://submission.herokuapp.com/creator-cards/:slugNote: This assessment is a standalone technical exercise. It is not a reflection of the products or domain you will actually work on.
| Field | Type | Constraints | Description |
|---|---|---|---|
id | string | ULID | Stored as _id in MongoDB, but ALWAYS serialized as id in API responses |
title | string | 3ā100 characters | e.g. "George Cooks" |
description | string | max 500 characters | e.g. "George Cooks is a weekly cooking podcast by Chef George AmadiObi" |
slug | string | 5ā50 characters; unique; letters, numbers, hyphens and underscores only | Public identifier used for card retrieval |
creator_reference | string | exactly 20 characters | Identifies the creator on the consuming service |
links[] | array of objects | ā | Links the creator wants to showcase |
links[].title | string | 1ā100 characters | Title of the link |
links[].url | string | max 200 characters; must start with http:// or https:// | Link URL |
service_rates | object | ā | Rates offered by the creator for services |
service_rates.currency | string | enum: NGN | USD | GBP | GHS | Currency for all rates on the card |
service_rates.rates[] | array of objects | non-empty if service_rates is present | Individual service rates |
service_rates.rates[].name | string | 3ā100 characters | e.g. "IG Story Post" |
service_rates.rates[].description | string | max 250 characters | Description of the service |
service_rates.rates[].amount | number | positive integer (min 1) | Minor units: kobo for NGN, cents for USD, pence for GBP, pesewas for GHS |
status | string | enum: draft | published | Drafts can NEVER be retrieved via the public endpoint |
access_type | string | enum: public | private | Defaults to public |
access_code | string | exactly 6 alphanumeric characters | Required if access_type is private |
created | number | ā | Unix epoch milliseconds |
updated | number | ā | Unix epoch milliseconds |
deleted | number | null | ā | null unless the card has been deleted |
_id vs id: In MongoDB the document identifier lives in the _id field, per MongoDB convention. However, all front-facing API responses must expose it as id. Your serialization layer is responsible for this mapping - a response containing _id is incorrect.POST /creator-cards{
"title": "George Cooks",
"description": "George Cooks is a weekly cooking podcast by Chef George AmadiObi",
"slug": "george-cooks",
"creator_reference": "crt_8f2k1m9x4p7w3q5z",
"links": [
{"title": "YouTube Channel", "url": "https://youtube.com/@georgecooks"},
{"title": "Instagram", "url": "https://instagram.com/georgecooks"}
],
"service_rates": {
"currency": "NGN",
"rates": [
{"name": "IG Story Post", "description": "One Instagram story mention", "amount": 5000000},
{"name": "Recipe Feature", "description": "Featured recipe segment on the podcast", "amount": 15000000}
]
},
"status": "published",
"access_type": "public"
}| Field | Required | Rules |
|---|---|---|
title | Yes | String, 3-100 characters |
description | No | String, max 500 characters |
slug | No | 5-50 characters; letters, numbers, hyphens (-) and underscores (_) only; must be unique across all cards |
creator_reference | Yes | String of exactly 20 characters |
links | No | Array; each entry must have a title (1-100 chars) and a valid url (max 200 chars) starting with http:// or https:// |
service_rates | No | If present: currency must be one of NGN, USD, GBP, GHS; rates must be a non-empty array; each rate must have a name (3-100 chars), a description (max 250 chars), and an amount that is a positive integer (minor units - no decimals, no negatives, no zero) |
status | Yes | Must be exactly draft or published |
access_type | No | Must be public or private if present; defaults to public when omitted |
access_code | Conditional | Required if access_type is private; must be exactly 6 alphanumeric characters (letters and numbers only). Must NOT be provided when access_type is public or omitted |
slug is omitted, your service must auto-generate one from the title:cook-a8x2k1)slug IS provided by the client and is already taken, return the SL02 error - do NOT silently modify a client-provided slug.{
"status": "success",
"message": "Creator Card Created Successfully.",
"data": {
"id": "01JG8XYZA2B3C4D5E6F7G8H9J0",
"title": "George Cooks",
"description": "George Cooks is a weekly cooking podcast by Chef George AmadiObi",
"slug": "george-cooks",
"creator_reference": "crt_8f2k1m9x4p7w3q5z",
"links": [
{"title": "YouTube Channel", "url": "https://youtube.com/@georgecooks"},
{"title": "Instagram", "url": "https://instagram.com/georgecooks"}
],
"service_rates": {
"currency": "NGN",
"rates": [
{"name": "IG Story Post", "description": "One Instagram story mention", "amount": 5000000},
{"name": "Recipe Feature", "description": "Featured recipe segment on the podcast", "amount": 15000000}
]
},
"status": "published",
"access_type": "public",
"access_code": null,
"created": 1767052800000,
"updated": 1767052800000,
"deleted": null
}
}access_code is returned in the creation response (the creator needs to know it), but it is NEVER returned by the public retrieval endpoint.{
"status": "error",
"message": "Slug is already taken",
"code": "SL02"
}GET /creator-cards/:slugNF01status is draft ā HTTP 404, error code NF02 (drafts are not publicly retrievable; the distinct code lets API callers distinguish "does not exist" from "exists but is a draft")private and no access_code query parameter was supplied ā HTTP 403, error code AC03private and the supplied access_code does not match ā HTTP 403, error code AC04GET /creator-cards/george-cooks?access_code=A1B2C3{
"status": "success",
"message": "Creator Card Retrieved Successfully.",
"data": {
"id": "01JG8XYZA2B3C4D5E6F7G8H9J0",
"title": "George Cooks",
"description": "George Cooks is a weekly cooking podcast by Chef George AmadiObi",
"slug": "george-cooks",
"creator_reference": "crt_8f2k1m9x4p7w3q5z",
"links": [
{"title": "YouTube Channel", "url": "https://youtube.com/@georgecooks"}
],
"service_rates": {
"currency": "NGN",
"rates": [
{"name": "IG Story Post", "description": "One Instagram story mention", "amount": 5000000}
]
},
"status": "published",
"access_type": "public",
"created": 1767052800000,
"updated": 1767052800000,
"deleted": null
}
}access_code field is OMITTED entirely from retrieval responses, even for private cards accessed with the correct pin. The identifier is exposed as id, never _id.{
"status": "error",
"message": "Creator card not found",
"code": "NF01"
}DELETE /creator-cards/:slug{
"creator_reference": "crt_8f2k1m9x4p7w3q5z"
}| Field | Required | Rules |
|---|---|---|
creator_reference | Yes | String of exactly 20 characters |
NF01GET /creator-cards/:slug returns HTTP 404, NF01){
"status": "success",
"message": "Creator Card Deleted Successfully.",
"data": {
"id": "01JG8XYZA2B3C4D5E6F7G8H9J0",
"title": "George Cooks",
"description": "George Cooks is a weekly cooking podcast by Chef George AmadiObi",
"slug": "george-cooks",
"creator_reference": "crt_8f2k1m9x4p7w3q5z",
"links": [
{"title": "YouTube Channel", "url": "https://youtube.com/@georgecooks"},
{"title": "Instagram", "url": "https://instagram.com/georgecooks"}
],
"service_rates": {
"currency": "NGN",
"rates": [
{"name": "IG Story Post", "description": "One Instagram story mention", "amount": 5000000},
{"name": "Recipe Feature", "description": "Featured recipe segment on the podcast", "amount": 15000000}
]
},
"status": "published",
"access_type": "public",
"access_code": null,
"created": 1767052800000,
"updated": 1767052800000,
"deleted": 1767139200000
}
}| Business Rule | Error Code | HTTP Code | Example Message |
|---|---|---|---|
| Slug must be unique across all cards | SL02 | 400 | "Slug is already taken" |
| access_code is required when access_type is private | AC01 | 400 | "access_code is required when access_type is private" |
| access_code must not be set on public cards | AC05 | 400 | "access_code can only be set on private cards" |
| Card with the given slug does not exist | NF01 | 404 | "Creator card not found" |
| Card exists but is in draft status | NF02 | 404 | "Creator card not found" |
| Access code required to view private card | AC03 | 403 | "This card is private. An access code is required" |
| Invalid access code | AC04 | 403 | "Invalid access code" |
message field should contain a clear, human-readable message (never empty)code must match exactly as specified in the table aboveNF01 and NF02 both return HTTP 404 with similar messages - the code is what allows an API caller to tell them apart_id internally per MongoDB convention; API responses must expose idPOST {base_url}/creator-cards, NOT POST {base_url}/v1/creator-cards or POST {base_url}/api/creator-cardsPOST /creator-cards
{
"title": "George Cooks",
"description": "Weekly cooking podcast",
"slug": "george-cooks",
"creator_reference": "crt_8f2k1m9x4p7w3q5z",
"links": [{"title": "YouTube", "url": "https://youtube.com/@georgecooks"}],
"service_rates": {
"currency": "NGN",
"rates": [{"name": "IG Story Post", "description": "One story mention", "amount": 5000000}]
},
"status": "published"
}id (not _id)POST /creator-cards
{
"title": "Ada Designs Things",
"creator_reference": "crt_a1b2c3d4e5f6g7h8",
"status": "published"
}POST /creator-cards
{
"title": "VIP Rate Card",
"creator_reference": "crt_x9y8z7w6v5u4t3s2",
"status": "published",
"access_type": "private",
"access_code": "A1B2C3"
}GET /creator-cards/george-cooksidGET /creator-cards/vip-rate-card?access_code=A1B2C3DELETE /creator-cards/ada-designs-things
{
"creator_reference": "crt_a1b2c3d4e5f6g7h8"
}deleted setPOST /creator-cards
{
"title": "Another George",
"slug": "george-cooks",
"creator_reference": "crt_m1n2b3v4c5x6z7l8",
"status": "published"
}POST /creator-cards
{
"title": "Secret Card",
"creator_reference": "crt_q1w2e3r4t5y6u7i8",
"status": "published",
"access_type": "private"
}POST /creator-cards
{
"title": "Public Card",
"creator_reference": "crt_q1w2e3r4t5y6u7i8",
"status": "published",
"access_type": "public",
"access_code": "A1B2C3"
}POST /creator-cards
{
"title": "Bad Status Card",
"creator_reference": "crt_q1w2e3r4t5y6u7i8",
"status": "archived"
}GET /creator-cards/does-not-exist-123GET /creator-cards/my-draft-cardGET /creator-cards/vip-rate-cardGET /creator-cards/vip-rate-card?access_code=WRONG1DELETE /creator-cards/does-not-exist-123
{
"creator_reference": "crt_q1w2e3r4t5y6u7i8"
}GET /creator-cards/ada-designs-thingsid, never _id/creator-cards, not /v1/creator-cards)https://submission.herokuapp.com)