Keepable docs
Sender API

Content types in depth

The content types the send endpoint accepts, their stable vs preview status, and the structured attributes each carries. Grounded in the Nigerian market.

Every content delivery names a content_type. It does two things: it tells the recipient app how to frame the item (icon, grouping, the by-type filter), and it selects which attributes schema applies.

The envelope is identical for every type. content_type is not a discriminator that changes the required envelope fields; it selects the optional attributes shape. Adding a new content type is an additive change; it never disturbs the envelope or existing types.

Stable vs preview

The vocabulary is split by release status:

StatusTypes
Stableletter, payslip, invoice, statement
Previewcredit_note, booking, receipt, form, survey_invitation, registered_letter, processing_notice, document_request, document_return
  • Stable types are production-ready; their semantics and attributes are covered by the deprecation policy.
  • Preview types are visible and usable, but their semantics and attributes shape may still change. Don't depend on them in production yet.

The status is also machine-readable in the spec as x-keepable-status, so tooling can surface it.

An unknown content_type is rejected with 422. The recipient's by-type filter relies on the value, so the set is closed and validated.

campaign is not a content type. Promotional banners are a separate overlay entity. See Campaigns. A delivery opts into a campaign by carrying a campaign_tag in its metadata, never by setting content_type: campaign.

Why these are the stable four

The stable set is the highest-frequency, identity-anchored mail in the Nigerian market:

  • payslip: issuing payslips is a statutory payroll record (the Labour Act, the Personal Income Tax Act, the Pension Reform Act), so every formal employer produces them monthly.
  • invoice: FIRS National E-Invoicing is mandatory for large taxpayers from August 2025 and extends to smaller VAT-registered businesses through 2026. A cleared e-invoice can travel to the inbox carrying its clearance reference (irn).
  • statement: monthly bank statements are a customer right, and a signed, identity-anchored channel is a direct answer to the "fake credit alert" fraud the CBN repeatedly warns about. Pension (RSA) statements fit here too.
  • letter: the generic baseline for anything without a more specific type.

Attributes by type

attributes carries the structured, machine-readable fields the recipient app renders into a rich view. Five types have a typed schema today; the rest accept a mostly free-form attributes object, except that a few preview types still require specific fields (for example form requires a form_id, and document_request requires form_id plus purpose and retention_note — each rejected 422 without them).

Put anything the recipient must see or act on (an amount, a due date, a location) in attributes or a part, never in metadata, which Keepable never reads or renders.

payslip (stable)

FieldRequiredNotes
pay_periodyesThe period covered, e.g. "2026-03" (YYYY-MM).
net_payyesTake-home pay as a decimal string. Never a float.
gross_paynoGross pay as a decimal string.
currencyyesISO 4217 code, e.g. NGN.
{ "pay_period": "2026-03", "gross_pay": "320000.00", "net_pay": "250000.00", "currency": "NGN" }

invoice (stable)

FieldRequiredNotes
amountyesTotal due as a decimal string.
currencyyesISO 4217 code.
due_dateyesISO 8601 date (YYYY-MM-DD).
invoice_numbernoYour invoice reference.
irnnoFIRS Invoice Reference Number from e-invoicing clearance, where applicable.
{ "amount": "125000.00", "currency": "NGN", "due_date": "2026-06-30", "invoice_number": "INV-88213", "irn": "IRN-7F3A9C20-2026" }

statement (stable)

FieldRequiredNotes
period_startyesISO 8601 date.
period_endyesISO 8601 date.
currencyyesISO 4217 code.
opening_balancenoDecimal string.
closing_balancenoDecimal string.
account_referencenoThe account or RSA reference the statement covers.
{ "period_start": "2026-03-01", "period_end": "2026-03-31", "currency": "NGN", "opening_balance": "10000.00", "closing_balance": "42500.50" }

booking (preview)

A calendar-ready appointment: a clinic visit, an enrollment slot, a virtual hearing or interview. The booking's human title is the delivery subject, not an attribute.

FieldRequiredNotes
starts_atyesRFC 3339 date-time. Not required to be in the future — validation is clock-free so idempotent retries never flip to 422.
ends_atnoRFC 3339 date-time. Must be strictly after starts_at.
locationnoPhysical venue or address, when there is one.
meeting_urlnoThe virtual join link. https only, no userinfo.
meeting_codenoPlatform-native meeting id/code (Zoom meeting ID, Meet code, Teams meeting ID) the recipient can key in without a URL.
meeting_passcodenoMeeting passcode, stored and rendered verbatim (case matters). Never sufficient on its own.
meeting_dial_innoTelephone join path, optionally with a one-tap dial tail, e.g. "+234 1 227 8900,,832#".
referencenoYour booking reference, which the recipient may need to present.

A booking must carry at least one reachable coordinatelocation, meeting_url, meeting_code, or meeting_dial_in — so the recipient can always reach the room, physical or virtual. Any combination (a hybrid hearing with both a venue and a link) is accepted. A meeting_passcode alone is rejected 422.

{ "starts_at": "2026-04-02T10:30:00Z", "ends_at": "2026-04-02T11:00:00Z", "location": "Lagos Island branch", "reference": "BK-4471" }
virtual
{ "starts_at": "2026-04-02T10:30:00Z", "meeting_url": "https://zoom.us/j/9876543210", "meeting_code": "987 654 3210", "meeting_passcode": "aB3xZ9", "meeting_dial_in": "+234 1 227 8900,,987#" }

Recipient apps render the tap-to-join affordance from the typed meeting_* fields only — links in the message body are never promoted to a Join button. This is deliberate anti-phishing: put the join link in meeting_url, not the body, so recipients learn to join from the sealed mailpiece.

Every booking is also a presentable artifact. When your delivery includes no PDF part, Keepable renders a deterministic sealed booking slip from these fields (identity plate, date, location and/or join coordinates, reference), file-hash-verifiable through /public/verify. Attach your own PDF to keep your attestation. Recipients also get an add-to-calendar action (a server-side .ics projection or native calendar insert) and a single reminder the day before.

processing_notice (preview)

The compliance fields a controller must disclose when it processes personal data under a non-consent lawful basis (NDPC GAID 2025 Art. 18(2), ADR 0068). These project into the notice's ISO 27560 receipt. For consent, use a consent request instead, not this type.

FieldRequiredNotes
lawful_basisyesOne of contract, legal_obligation, legitimate_interest, vital_interest, public_interest. consent is rejected 422.
purposeyesPlain-language purpose, shown to the data subject and recorded on the receipt.
data_categoriesyesNon-empty array of the data categories processed, e.g. ["full name", "bvn"].
retention_noteyesPlain-language retention disclosure.
retention_floor_yearsnoSector retention floor in years; drives the receipt's retain_until.
authority_namenoThe authority the processing answers to, e.g. "NDPC", "CBN".
authority_referencenoA reference for that authority.
{
  "lawful_basis": "legal_obligation",
  "purpose": "Anti-money-laundering monitoring of your account",
  "data_categories": ["full name", "bvn", "transaction history"],
  "retention_note": "Retained 5 years after the relationship ends (MLPPA 2022 s.8).",
  "retention_floor_years": 5,
  "authority_name": "CBN"
}

document_request (preview)

Asks the recipient to return documents (and answers), rather than just read something. It is a form whose template carries document fields. Because collecting documents is a data collection under the NDPA, the disclosure framing travels with the ask:

FieldRequiredNotes
form_idyesThe template being requested, as form uses it.
purposeyesPlain-language reason you are collecting the documents.
retention_noteyesPlain-language retention disclosure for the copies you receive.

Full field types, the two ways a recipient satisfies a document field, the sealed return bundle, and response tracking are covered in Forms. document_return is the sealed bundle a submission produces; it is Keepable-authored and delivered to the recipient's vault, never sent by you.

Money, dates, currencies

Across every typed schema: money is a decimal string ("125000.00", never a float), dates are ISO 8601 (YYYY-MM-DD), date-times are RFC 3339, and currencies are ISO 4217 codes (NGN). Violations are rejected 422.

Adding a content type

New types graduate from preview to stable as their semantics settle, and new preview types appear as the market calls for them (receipt and registered_letter are reserved this way as forward-looking preview types). Because a type is just a vocabulary entry plus an optional attributes schema, adding one never changes the envelope or any existing type, so your integration keeps working untouched.

When you need a signature

If the recipient must sign rather than just read, that is not a content type at all: use an agreement, which returns a signed, tamper-evident covenant.