{"openapi":"3.1.0","info":{"title":"regulation-control","version":"0.1.0","description":"Detect regulatory changes and map them to obligations, policies, controls, products and evidence requirements."},"servers":[{"url":"https://controlgraph-api.com"}],"paths":{"/health":{"get":{"summary":"Liveness and deployed version","operationId":"getHealth","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/":{"get":{"summary":"Service index — endpoints, auth and error format","operationId":"get/","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/changes":{"post":{"summary":"Map regulatory changes onto your obligations, controls, evidence and deadlines","description":"Takes a regulatory change you have already read — citation, dates, and the obligations it creates — plus your own control inventory, and returns the impact graph: which controls each obligation touches, which obligations have no control at all, what evidence is now required, and the dated deadlines. Coverage is deny-by-default: an obligation with no matching control is a GAP, never assumed covered. Matching is a stated set rule over tags, not a similarity score, and every graph edge carries the reason it exists. Send one \"change\" or up to 50 in \"changes\"; the \"controls\" inventory is shared across the batch. Billed one unit per change mapped. Nothing is stored. This is arithmetic and graph mapping over inputs you supply. It does not read, interpret or apply law, and it is not legal advice.","operationId":"postV1Changes","security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"dueSoonWindowDays":{"type":"integer"},"changes":{"type":"array","items":{"type":"object","properties":{"changeId":{"type":"string"},"citation":{"type":"string"},"evidenceCutoff":{"type":"string","description":"Evidence collected before this cannot satisfy the change. Null for guidance and enforcement actions."},"summary":{"type":"object","description":"Counts of covered / weak / gap, controls touched and untouched, teams, the highest priority band, the earliest deadline and how many have passed."},"obligations":{"type":"array","items":{"type":"object"},"description":"Sorted by priority score descending, then soonest compliance date. Each carries coverage, matched controls with the tags they matched on, required evidence, every finding, three dated deadlines and a reproducible priority derivation."},"graph":{"type":"object","description":"nodes[] and edges[]. Every edge carries a \"reason\" naming why it exists — a mapping you can defend rather than one you have to trust."},"unmatchedControls":{"type":"array","items":{"type":"string"},"description":"Controls in your inventory this change does not touch."},"exposure":{"type":"object","description":"Your recorded penalties for obligations that are not covered, allocated to owning teams. byOwner amounts plus unassignedMinor sum EXACTLY to totalMinor."},"warnings":{"type":"array","items":{"type":"string"},"description":"Structurally valid but worth a human look — a tag no control carries, a lead time longer than the notice given, a retroactive date."}}}},"notice":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["controls"],"properties":{"controls":{"type":"array","maxItems":500,"items":{"type":"object","required":["controlId","name","status","tags"],"properties":{"controlId":{"type":"string","description":"Your id. Must be unique within the inventory."},"name":{"type":"string"},"owner":{"type":"string","description":"Owning team. A control with no owner is reported — nobody will action it."},"status":{"type":"string","enum":["implemented","planned","in_remediation","retired"],"description":"Only \"implemented\" can cover an obligation."},"tags":{"type":"array","minItems":1,"maxItems":32,"items":{"type":"string"},"description":"Scope tags. A control matches an obligation when its tags contain EVERY tag the obligation requires. Trimmed, lower-cased, de-duplicated and sorted on the way in."},"lastTestedAt":{"type":"string","format":"date-time","description":"Missing is control_untested — there is nothing to show an auditor."},"testFrequencyDays":{"type":"integer","minimum":1,"maximum":3650,"description":"How often this control must be tested. Defaults to 365."},"evidence":{"type":"array","maxItems":32,"items":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["policy_document","control_test_result","system_configuration","audit_log","training_record","attestation","risk_assessment","third_party_report","notification_record","retention_schedule"],"description":"One of the catalogued evidence kinds. GET /v1/obligation-types lists them."},"artifactId":{"type":"string","description":"Your reference for the document, ticket or export. Echoed back on the graph edge."},"collectedAt":{"type":"string","format":"date-time","description":"When it was collected. Undated evidence dates nothing and is reported as evidence_undated."}}}}}},"description":"Your control inventory. Required; may be empty, which returns every obligation as a gap."},"change":{"type":"object","required":["changeId","citation","title","jurisdiction","changeType","publishedAt","effectiveAt","obligations"],"properties":{"changeId":{"type":"string"},"citation":{"type":"string","description":"The citation as you would put it in a filing, e.g. \"12 CFR 1026.19(e)\"."},"title":{"type":"string"},"jurisdiction":{"type":"string","description":"Free-form, e.g. \"US\", \"EU\", \"US-CA\". Echoed, never interpreted."},"regulator":{"type":"string"},"changeType":{"type":"string","enum":["new_rule","amendment","guidance","enforcement_action"],"description":"Load-bearing: new_rule and amendment set an evidence cut-off at publishedAt, because evidence gathered before a new requirement existed cannot demonstrate it. guidance and enforcement_action set none."},"publishedAt":{"type":"string","format":"date-time","description":"When the change was published. Sets the evidence cut-off for new rules and amendments."},"effectiveAt":{"type":"string","format":"date-time"},"transitionEndsAt":{"type":"string","format":"date-time","description":"End of any transition or grace period. Must not be before effectiveAt — a grace period cannot pull a deadline forward."},"summary":{"type":"string"},"currency":{"type":"string","description":"ISO-4217. Required when any obligation records a maxPenaltyMinor."},"obligations":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["obligationId","type","text","appliesTo"],"properties":{"obligationId":{"type":"string","description":"Your id. Must be unique within the change."},"type":{"type":"string","enum":["disclosure","recordkeeping","reporting","notification","consent","data_protection","access_control","monitoring","risk_assessment","governance","training","third_party_oversight","prohibition"],"description":"Sets the default preparation lead time and the default evidence kinds."},"text":{"type":"string","description":"The obligation in your own words. Echoed, never parsed or interpreted."},"appliesTo":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"string"},"description":"Every tag a control must carry to match. Required and non-empty: an obligation with no tags could match nothing and would be a permanent gap."},"requiresEvidence":{"type":"array","items":{"type":"string","enum":["policy_document","control_test_result","system_configuration","audit_log","training_record","attestation","risk_assessment","third_party_report","notification_record","retention_schedule"]},"description":"Overrides the obligation type's default evidence kinds."},"enforcement":{"type":"string","enum":["penalty","supervisory","guidance"],"description":"How the rule is enforced — the largest term in the priority score. Defaults to \"supervisory\", and the default is stated in the derivation."},"dueAt":{"type":"string","format":"date-time","description":"An obligation-specific date, replacing the change's effective date for this obligation."},"preparationDays":{"type":"integer","minimum":0,"maximum":3650,"description":"Overrides the obligation type's catalogued lead time."},"maxPenaltyMinor":{"type":"integer","minimum":0,"description":"Maximum penalty you have recorded for failing this obligation, in INTEGER minor units. A fractional amount is a 400, not a rounding. Requires \"currency\" on the change."}}}},"metadata":{"type":"object"}}},"changes":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["changeId","citation","title","jurisdiction","changeType","publishedAt","effectiveAt","obligations"],"properties":{"changeId":{"type":"string"},"citation":{"type":"string","description":"The citation as you would put it in a filing, e.g. \"12 CFR 1026.19(e)\"."},"title":{"type":"string"},"jurisdiction":{"type":"string","description":"Free-form, e.g. \"US\", \"EU\", \"US-CA\". Echoed, never interpreted."},"regulator":{"type":"string"},"changeType":{"type":"string","enum":["new_rule","amendment","guidance","enforcement_action"],"description":"Load-bearing: new_rule and amendment set an evidence cut-off at publishedAt, because evidence gathered before a new requirement existed cannot demonstrate it. guidance and enforcement_action set none."},"publishedAt":{"type":"string","format":"date-time","description":"When the change was published. Sets the evidence cut-off for new rules and amendments."},"effectiveAt":{"type":"string","format":"date-time"},"transitionEndsAt":{"type":"string","format":"date-time","description":"End of any transition or grace period. Must not be before effectiveAt — a grace period cannot pull a deadline forward."},"summary":{"type":"string"},"currency":{"type":"string","description":"ISO-4217. Required when any obligation records a maxPenaltyMinor."},"obligations":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["obligationId","type","text","appliesTo"],"properties":{"obligationId":{"type":"string","description":"Your id. Must be unique within the change."},"type":{"type":"string","enum":["disclosure","recordkeeping","reporting","notification","consent","data_protection","access_control","monitoring","risk_assessment","governance","training","third_party_oversight","prohibition"],"description":"Sets the default preparation lead time and the default evidence kinds."},"text":{"type":"string","description":"The obligation in your own words. Echoed, never parsed or interpreted."},"appliesTo":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"string"},"description":"Every tag a control must carry to match. Required and non-empty: an obligation with no tags could match nothing and would be a permanent gap."},"requiresEvidence":{"type":"array","items":{"type":"string","enum":["policy_document","control_test_result","system_configuration","audit_log","training_record","attestation","risk_assessment","third_party_report","notification_record","retention_schedule"]},"description":"Overrides the obligation type's default evidence kinds."},"enforcement":{"type":"string","enum":["penalty","supervisory","guidance"],"description":"How the rule is enforced — the largest term in the priority score. Defaults to \"supervisory\", and the default is stated in the derivation."},"dueAt":{"type":"string","format":"date-time","description":"An obligation-specific date, replacing the change's effective date for this obligation."},"preparationDays":{"type":"integer","minimum":0,"maximum":3650,"description":"Overrides the obligation type's catalogued lead time."},"maxPenaltyMinor":{"type":"integer","minimum":0,"description":"Maximum penalty you have recorded for failing this obligation, in INTEGER minor units. A fractional amount is a 400, not a rounding. Requires \"currency\" on the change."}}}},"metadata":{"type":"object"}}}}}},"example":{"controls":[{"controlId":"CTL-014","name":"Consumer disclosure template review","owner":"Payments Compliance","status":"implemented","tags":["payments","disclosure","us"],"lastTestedAt":"2026-07-01","testFrequencyDays":180,"evidence":[{"kind":"policy_document","artifactId":"POL-221","collectedAt":"2026-07-02"},{"kind":"notification_record","artifactId":"NR-9930","collectedAt":"2026-07-14"}]},{"controlId":"CTL-027","name":"Fee change customer notification job","owner":"Payments Engineering","status":"planned","tags":["payments","notification","us"],"testFrequencyDays":90},{"controlId":"CTL-101","name":"Account record retention schedule","owner":"Records Management","status":"implemented","tags":["recordkeeping","us"],"lastTestedAt":"2024-02-01","evidence":[{"kind":"retention_schedule","artifactId":"RS-04","collectedAt":"2024-02-01"}]}],"change":{"changeId":"REG-2026-0117","citation":"12 CFR 1026.19(e)","title":"Revised timing and content of cost-of-credit disclosures","jurisdiction":"US","regulator":"CFPB","changeType":"amendment","publishedAt":"2026-03-02","effectiveAt":"2026-09-01","transitionEndsAt":"2026-12-01","currency":"USD","obligations":[{"obligationId":"OB-1","type":"disclosure","text":"Provide the revised cost-of-credit disclosure within three business days of application.","appliesTo":["payments","disclosure"],"enforcement":"penalty","maxPenaltyMinor":500000000},{"obligationId":"OB-2","type":"notification","text":"Notify existing customers of the fee schedule change at least 45 days before it applies.","appliesTo":["payments","notification"],"enforcement":"penalty","maxPenaltyMinor":250000000},{"obligationId":"OB-3","type":"recordkeeping","text":"Retain each issued disclosure for five years and produce it on request.","appliesTo":["recordkeeping","disclosure"],"enforcement":"supervisory"}]}}}}},"x-quota-cost":0}},"/v1/demo/map":{"post":{"summary":"Public demo — map one change with no API key","description":"The real engine, capped at one change, 5 obligations and 20 controls. Nothing is stored, nothing is metered, no key is required. This is arithmetic and graph mapping over inputs you supply. It does not read, interpret or apply law, and it is not legal advice.","operationId":"postV1DemoMap","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"change":{"type":"object","properties":{"changeId":{"type":"string"},"citation":{"type":"string"},"evidenceCutoff":{"type":"string","description":"Evidence collected before this cannot satisfy the change. Null for guidance and enforcement actions."},"summary":{"type":"object","description":"Counts of covered / weak / gap, controls touched and untouched, teams, the highest priority band, the earliest deadline and how many have passed."},"obligations":{"type":"array","items":{"type":"object"},"description":"Sorted by priority score descending, then soonest compliance date. Each carries coverage, matched controls with the tags they matched on, required evidence, every finding, three dated deadlines and a reproducible priority derivation."},"graph":{"type":"object","description":"nodes[] and edges[]. Every edge carries a \"reason\" naming why it exists — a mapping you can defend rather than one you have to trust."},"unmatchedControls":{"type":"array","items":{"type":"string"},"description":"Controls in your inventory this change does not touch."},"exposure":{"type":"object","description":"Your recorded penalties for obligations that are not covered, allocated to owning teams. byOwner amounts plus unassignedMinor sum EXACTLY to totalMinor."},"warnings":{"type":"array","items":{"type":"string"},"description":"Structurally valid but worth a human look — a tag no control carries, a lead time longer than the notice given, a retroactive date."}}},"dueSoonWindowDays":{"type":"integer"},"notice":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["controls","change"],"properties":{"controls":{"type":"array","maxItems":20,"items":{"type":"object","required":["controlId","name","status","tags"],"properties":{"controlId":{"type":"string","description":"Your id. Must be unique within the inventory."},"name":{"type":"string"},"owner":{"type":"string","description":"Owning team. A control with no owner is reported — nobody will action it."},"status":{"type":"string","enum":["implemented","planned","in_remediation","retired"],"description":"Only \"implemented\" can cover an obligation."},"tags":{"type":"array","minItems":1,"maxItems":32,"items":{"type":"string"},"description":"Scope tags. A control matches an obligation when its tags contain EVERY tag the obligation requires. Trimmed, lower-cased, de-duplicated and sorted on the way in."},"lastTestedAt":{"type":"string","format":"date-time","description":"Missing is control_untested — there is nothing to show an auditor."},"testFrequencyDays":{"type":"integer","minimum":1,"maximum":3650,"description":"How often this control must be tested. Defaults to 365."},"evidence":{"type":"array","maxItems":32,"items":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["policy_document","control_test_result","system_configuration","audit_log","training_record","attestation","risk_assessment","third_party_report","notification_record","retention_schedule"],"description":"One of the catalogued evidence kinds. GET /v1/obligation-types lists them."},"artifactId":{"type":"string","description":"Your reference for the document, ticket or export. Echoed back on the graph edge."},"collectedAt":{"type":"string","format":"date-time","description":"When it was collected. Undated evidence dates nothing and is reported as evidence_undated."}}}}}}},"change":{"type":"object","required":["changeId","citation","title","jurisdiction","changeType","publishedAt","effectiveAt","obligations"],"properties":{"changeId":{"type":"string"},"citation":{"type":"string","description":"The citation as you would put it in a filing, e.g. \"12 CFR 1026.19(e)\"."},"title":{"type":"string"},"jurisdiction":{"type":"string","description":"Free-form, e.g. \"US\", \"EU\", \"US-CA\". Echoed, never interpreted."},"regulator":{"type":"string"},"changeType":{"type":"string","enum":["new_rule","amendment","guidance","enforcement_action"],"description":"Load-bearing: new_rule and amendment set an evidence cut-off at publishedAt, because evidence gathered before a new requirement existed cannot demonstrate it. guidance and enforcement_action set none."},"publishedAt":{"type":"string","format":"date-time","description":"When the change was published. Sets the evidence cut-off for new rules and amendments."},"effectiveAt":{"type":"string","format":"date-time"},"transitionEndsAt":{"type":"string","format":"date-time","description":"End of any transition or grace period. Must not be before effectiveAt — a grace period cannot pull a deadline forward."},"summary":{"type":"string"},"currency":{"type":"string","description":"ISO-4217. Required when any obligation records a maxPenaltyMinor."},"obligations":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["obligationId","type","text","appliesTo"],"properties":{"obligationId":{"type":"string","description":"Your id. Must be unique within the change."},"type":{"type":"string","enum":["disclosure","recordkeeping","reporting","notification","consent","data_protection","access_control","monitoring","risk_assessment","governance","training","third_party_oversight","prohibition"],"description":"Sets the default preparation lead time and the default evidence kinds."},"text":{"type":"string","description":"The obligation in your own words. Echoed, never parsed or interpreted."},"appliesTo":{"type":"array","minItems":1,"maxItems":16,"items":{"type":"string"},"description":"Every tag a control must carry to match. Required and non-empty: an obligation with no tags could match nothing and would be a permanent gap."},"requiresEvidence":{"type":"array","items":{"type":"string","enum":["policy_document","control_test_result","system_configuration","audit_log","training_record","attestation","risk_assessment","third_party_report","notification_record","retention_schedule"]},"description":"Overrides the obligation type's default evidence kinds."},"enforcement":{"type":"string","enum":["penalty","supervisory","guidance"],"description":"How the rule is enforced — the largest term in the priority score. Defaults to \"supervisory\", and the default is stated in the derivation."},"dueAt":{"type":"string","format":"date-time","description":"An obligation-specific date, replacing the change's effective date for this obligation."},"preparationDays":{"type":"integer","minimum":0,"maximum":3650,"description":"Overrides the obligation type's catalogued lead time."},"maxPenaltyMinor":{"type":"integer","minimum":0,"description":"Maximum penalty you have recorded for failing this obligation, in INTEGER minor units. A fractional amount is a 400, not a rounding. Requires \"currency\" on the change."}}}},"metadata":{"type":"object"}}}}},"example":{"controls":[{"controlId":"CTL-014","name":"Consumer disclosure template review","owner":"Payments Compliance","status":"implemented","tags":["payments","disclosure","us"],"lastTestedAt":"2026-07-01","testFrequencyDays":180,"evidence":[{"kind":"policy_document","artifactId":"POL-221","collectedAt":"2026-07-02"},{"kind":"notification_record","artifactId":"NR-9930","collectedAt":"2026-07-14"}]},{"controlId":"CTL-027","name":"Fee change customer notification job","owner":"Payments Engineering","status":"planned","tags":["payments","notification","us"],"testFrequencyDays":90},{"controlId":"CTL-101","name":"Account record retention schedule","owner":"Records Management","status":"implemented","tags":["recordkeeping","us"],"lastTestedAt":"2024-02-01","evidence":[{"kind":"retention_schedule","artifactId":"RS-04","collectedAt":"2024-02-01"}]}],"change":{"changeId":"REG-2026-0117","citation":"12 CFR 1026.19(e)","title":"Revised timing and content of cost-of-credit disclosures","jurisdiction":"US","regulator":"CFPB","changeType":"amendment","publishedAt":"2026-03-02","effectiveAt":"2026-09-01","transitionEndsAt":"2026-12-01","currency":"USD","obligations":[{"obligationId":"OB-1","type":"disclosure","text":"Provide the revised cost-of-credit disclosure within three business days of application.","appliesTo":["payments","disclosure"],"enforcement":"penalty","maxPenaltyMinor":500000000},{"obligationId":"OB-2","type":"notification","text":"Notify existing customers of the fee schedule change at least 45 days before it applies.","appliesTo":["payments","notification"],"enforcement":"penalty","maxPenaltyMinor":250000000},{"obligationId":"OB-3","type":"recordkeeping","text":"Retain each issued disclosure for five years and produce it on request.","appliesTo":["recordkeeping","disclosure"],"enforcement":"supervisory"}]}}}}}}},"/v1/obligation-types":{"get":{"summary":"The code catalogue: obligation types, evidence kinds, findings and the priority weights","description":"Every enum the engine emits, with its meaning, plus the exact constants used to derive a priority score. Branch on these codes rather than on the human-readable prose, which may change. The weights are published here because a score you cannot recompute on paper is a score you cannot defend in an audit — and the engine reads these same numbers.","operationId":"getV1ObligationTypes","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"obligationTypes":{"type":"object","description":"Each with a label, a description, its preparation lead time in days, and its default evidence kinds."},"evidenceKinds":{"type":"object"},"changeTypes":{"type":"object"},"controlStatuses":{"type":"object"},"coverageStatuses":{"type":"object"},"findingCodes":{"type":"object"},"deadlineKinds":{"type":"object"},"deadlineStatuses":{"type":"object"},"priority":{"type":"object","description":"The four weights, the urgency bands and the band thresholds."},"matchingRule":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout":{"post":{"summary":"Start a hosted Square checkout for a paid tier","description":"Returns a Square-hosted payment URL for the requested tier. No API key is needed — a prospect has not got one yet. The email Square collects becomes the account, so a key issued later for the same address inherits the paid quota. Enterprise returns a contact route rather than a payment link.","operationId":"postV1Checkout","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"checkoutUrl":{"type":"string","format":"uri","description":"Square-hosted payment page. Redirect the buyer here."},"tier":{"type":"string"},"sku":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tier"],"properties":{"tier":{"type":"string","description":"developer | growth | scale | enterprise"}}},"example":{"tier":"developer"}}}}}},"/api/billing/webhook":{"post":{"summary":"Square billing events, forwarded by the shared hub","description":"Machine-to-machine only. The shared Square billing hub POSTs signed events here; the signature is verified over the exact request bytes. Not callable by API consumers — documented so the integration is auditable.","operationId":"postApiBillingWebhook","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/keys":{"post":{"summary":"Request a free sandbox API key (sends a verification email)","description":"Starts self-serve key issuance. A one-time claim token is emailed to the address you supply; POST it to /v1/keys/claim to receive the key. No key is returned here, and the response is the same whether or not that address already has an account — so this endpoint cannot be used to find out who has one. The email is verified because your account, your usage and your invoice are all keyed to it: issuing on an unverified address would let anyone mint a working key on your account and spend your allowance. Keep using the same address and a paid plan bought with it raises the keys you already hold rather than issuing new ones.","operationId":"postV1Keys","security":[],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["verification_sent"]},"email":{"type":"string","format":"email"},"expiresAt":{"type":"string","format":"date-time","description":"After this, request a new one."},"next":{"type":"string","description":"The endpoint that turns the emailed token into a key."},"message":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Where the claim token is sent. This becomes your account."},"name":{"type":"string","maxLength":60,"description":"Optional label for the key, e.g. \"staging\". Shown in GET /v1/keys."}}},"example":{"email":"you@example.com","name":"staging"}}}}},"get":{"summary":"List your API keys for this API","description":"Every key on YOUR account for this API — label, non-secret prefix, status, when it was created, when it was last used, and what it has consumed this period. Authenticate with any active key on the account; the account is taken from that key, never from a parameter, so this can only ever return your own keys. Keys you hold for other APIs in the portfolio are not listed here. Secrets are not returned and cannot be: only a peppered hash is stored. Free, and consumes no quota.","operationId":"getV1Keys","security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"product":{"type":"string"},"accountId":{"type":"string"},"keys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"The label given at creation."},"prefix":{"type":"string","description":"Non-secret leading characters, so you can tell two keys apart without holding either."},"status":{"type":"string","enum":["active","revoked"]},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"Null if the key has never been used."},"revokedAt":{"type":["string","null"],"format":"date-time"},"quotaPerPeriod":{"type":["integer","null"]},"usedInPeriod":{"type":"integer"},"current":{"type":"boolean","description":"True for the key that authenticated this request."}}}},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"x-quota-cost":0}},"/v1/keys/claim":{"post":{"summary":"Exchange an emailed claim token for the API key","description":"Spends the token emailed by POST /v1/keys and returns the key. The key is shown ONCE — only a peppered hash is stored, so it can never be read back; if you lose it, rotate. The key works only on this API, and the allowance it is issued with reflects your account: your paid plan when you have one, the free sandbox allowance when you do not.","operationId":"postV1KeysClaim","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string","description":"Shown once and never again. Store it now."},"keyId":{"type":"string","description":"Use this with /v1/keys/{id}/revoke and /v1/keys/{id}/rotate."},"product":{"type":"string","description":"The key works only on this API."},"quotaPerPeriod":{"type":["integer","null"]},"plan":{"type":"string","description":"The tier this allowance came from, or \"sandbox\" on the free tier."},"warning":{"type":"string"},"usage":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"The claim token from the verification email."}}},"example":{"token":"eyJ2IjoxLCJlIjoieW91QGV4YW1wbGUuY29tIn0.ZXhhbXBsZQ"}}}}}},"/v1/keys/{id}/revoke":{"post":{"summary":"Revoke one of your API keys","description":"Kills a key immediately: the very next request made with it is refused with 401 invalid_api_key. There is no propagation delay and no cache to wait out. You may revoke the key you are authenticating with — that is the correct move when it has leaked, though you will then need another key, or the email flow, to get back in; rotate does both in one call. A key belonging to another account, or to another API, answers 404 exactly as an unknown id does, so this cannot be used to probe for keys you do not own. Idempotent, and free.","operationId":"postV1KeysIdRevoke","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["revoked"]},"message":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"x-quota-cost":0}},"/v1/keys/{id}/rotate":{"post":{"summary":"Replace one of your API keys with a new secret","description":"Issues a replacement key and revokes the old one. This is the answer to a leaked or lost key: the new secret is returned once, the old secret stops working immediately, and the label carries over unless you supply a new one. The replacement is issued at your account's current allowance — your paid plan if you have one — not at the old key's. Rotating does not count against the active-key limit, because it replaces rather than adds. Free, and it works while you are out of quota.","operationId":"postV1KeysIdRotate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string","description":"The new secret. Shown once."},"keyId":{"type":"string"},"replaced":{"type":"string","description":"The id of the key that was revoked."},"product":{"type":"string"},"quotaPerPeriod":{"type":["integer","null"]},"plan":{"type":"string"},"warning":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":60,"description":"Optional new label. Defaults to the old key's."}}},"example":{"name":"production (rotated)"}}}},"x-quota-cost":0}},"/v1/usage":{"get":{"summary":"Your consumption and remaining allowance for this period","description":"Reports what this API key has consumed in the current quota period, the ceiling being enforced against it, and when that ceiling resets. Free, and it consumes no quota, so it still answers once you are getting 429 quota_exceeded — which is the point: compare \"used\" against \"ceiling\" and read \"period.resetsAt\" to tell a period you have simply exhausted from a plan that is no longer active, which shows as a \"status\" of canceled and a ceiling of 0. \"used\" counts billing units (see \"unit\"), not HTTP requests: a single request can consume several. All money is in integer minor units (cents), never a decimal. The period reported here is the quota period — the calendar month in UTC, resetting at 00:00 UTC on the 1st. Your invoice window is anchored to the date you subscribed and is a different window; the amounts here are what this period’s usage has accrued so far, not a bill.","operationId":"getV1Usage","security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"product":{"type":"string","description":"The API this key is bound to. A key works only on one."},"tier":{"type":["string","null"],"description":"The plan tier you are billed on, e.g. \"developer\". Null on a free sandbox key with no paid plan."},"status":{"type":"string","enum":["sandbox","active","past_due","canceled"],"description":"State of your plan. \"sandbox\" means no paid plan; \"canceled\" explains a ceiling of 0."},"unit":{"type":"string","description":"The billing unit this API meters. \"used\", \"included\", \"ceiling\" and \"remaining\" are all counted in these, not in requests."},"period":{"type":"object","description":"The quota period: the calendar month in UTC.","properties":{"start":{"type":"string","format":"date-time","description":"00:00 UTC on the 1st of the current month."},"resetsAt":{"type":"string","format":"date-time","description":"When \"used\" returns to 0 — 00:00 UTC on the 1st of next month."}}},"included":{"type":"integer","description":"Units your flat monthly fee already covers, before overage pricing applies. 0 without a paid plan."},"used":{"type":"integer","description":"Units consumed so far this period."},"ceiling":{"type":["integer","null"],"description":"The hard limit enforced against this key: requests are refused beyond it. Null means unlimited."},"remaining":{"type":["integer","null"],"description":"ceiling minus used, never below 0. Null when the ceiling is unlimited."},"overageSoFarMinor":{"type":"integer","description":"What this period’s usage beyond \"included\" has accrued, in minor units, before your spend cap is applied. 0 without a paid plan."},"spendCapMinor":{"type":"integer","description":"The most the overage part of one period can ever cost you, in minor units. Your flat base fee is separate and is never capped away."},"requestId":{"type":"string","description":"Quote this when reporting a problem."}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"x-quota-cost":0}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Your API key. Create one free at POST /v1/keys. Also accepted as the x-api-key header."}},"responses":{"Error":{"description":"Deterministic error. Branch on `code`; quote `requestId` when reporting a problem.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","requestId"],"properties":{"code":{"type":"string","enum":["invalid_api_key","missing_api_key","quota_exceeded","rate_limited","invalid_request","not_found","method_not_allowed","payload_too_large","conflict","internal_error"]},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true},"requestId":{"type":"string","format":"uuid"}}}}}}}}}}}