Webhooks today, and where the REST API stands
Reference documentation for the RFP.co API: authentication, resources, pagination, rate limits and the webhook events the platform emits.
Signed outbound webhooks are available now and documented in full below — the header, the algorithm and a verifier you can copy. The REST API is a different matter: it is included in three plans and it is not built, which this page says plainly rather than leaving you to discover after subscribing.
What exists, and what does not
| Surface | Status | What you can do with it |
|---|---|---|
| Outbound webhooks | Available | Receive signed events at your own endpoint, verified with the recipe below |
| Slack delivery | Available | The same events into a channel, without writing a receiver |
| Integrations | Available | CRM, document storage and single sign-on connections configured in the workspace |
| REST API | Not built | Nothing yet — keys cannot be issued on any plan |
Do not build against the application’s own endpoints
The commercial machinery behind the REST API is real — the entitlement keys, the usage metering, the tenant scoping and the token hashing all exist and are tested. What is missing is the public surface itself: the credential, the request pipeline and the routes.
That gap is stated here rather than left quiet because a plan listing an allowance for something that does not exist is a promise. The honest options were to stop selling it or to document where it stands, and this is the second.
The events a webhook can carry
An endpoint subscribes to categories rather than to individual events, and the same delivery preferences that route a notification to email or Slack route it to a webhook. Some events are inherently per-workspace and some are per-pursuit; the audience rules decide who receives one, and a webhook receives what its subscribed categories produce.
| Category | Events | Fires when |
|---|---|---|
| New work found | Opportunity matched · Score threshold crossed | A published solicitation scores against one of your profiles |
| Signals and predictions | Buyer activity · Strong signal · Prediction published | Evidence of demand appears before anything is solicited |
| Changes to your pursuits | Opportunity amended | A buyer amends a solicitation you are tracking |
| Deadlines | Opportunity deadline · Submission deadline | A closing date or an internal submission date approaches |
| Work assigned to you | Task assigned · Review requested · Approval requested | A section changes hands inside the workspace |
| Documents and delivery | Export ready | A generated document or package finishes |
| Billing | Allowance threshold · Payment failed | A plan allowance nears its limit, or a charge is declined |
Delivery is per-recipient and respects each person’s preferences and digest schedule. A webhook endpoint is a workspace-level destination rather than a person, so it receives events as they happen rather than batched into a digest.
Verifying a delivery
A webhook endpoint is a URL on the public internet that accepts POSTs and acts on them, and URLs leak — through a log, a proxy, a screenshot in a support ticket. Every delivery is therefore signed, and an unverified body should be discarded rather than trusted.
The scheme is Stripe’s, deliberately and without modification. Not because it is clever, but because most backend developers have already written the five lines that check it, and inventing a scheme here would cost you an afternoon and gain nothing.
RFP-Signature: t=1786550400,v1=<hex>
signature = HMAC-SHA256(secret, `${t}.${rawBody}`)Capture the raw bytes before anything parses them
Parsing and re-serializing produces different bytes — key order, number formatting, unicode escapes — and every one of those differences fails the check. This is the single most common reason a verifier that looks correct rejects everything.
Parse the header into a timestamp and one or more signatures
More than one v1 value can be present during a secret rotation. Treat the header as a list, and accept the delivery if any signature matches any of your active secrets.
Compute the expected signature and compare in constant time
HMAC-SHA256 over the timestamp, a full stop, and the raw body, keyed with the endpoint secret. Compare with a timing-safe comparison rather than string equality.
Check the signature before the timestamp
A body whose signature does not match is unsigned, not stale, and the order matters: telling somebody their clock is wrong is the wrong answer to give a person who is being attacked.
Reject anything outside the tolerance window
Five minutes is the suggested window, and it is your choice rather than ours. Without it a captured body can be replayed indefinitely, because a signature alone never expires.
Deduplicate on the event id
Retries are real and a delivery can arrive twice. The event id in the body is the key to deduplicate on if you would rather do that than expire.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 300
export const verify = (header, rawBody, secrets) => {
const parts = Object.fromEntries(
header.split(',').map((pair) => pair.split('=', 2)),
)
const timestamp = Number(parts.t)
if (!Number.isFinite(timestamp)) return false
const provided = header
.split(',')
.filter((pair) => pair.startsWith('v1='))
.map((pair) => pair.slice(3))
const matched = secrets.some((secret) => {
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
return provided.some((signature) => {
const a = Buffer.from(signature, 'utf8')
const b = Buffer.from(expected, 'utf8')
return a.length === b.length && timingSafeEqual(a, b)
})
})
// Signature first: a body that does not match is unsigned, not stale.
if (!matched) return false
return Math.abs(Date.now() / 1000 - timestamp) <= TOLERANCE_SECONDS
}Secrets, rotation and failure
A secret is shown once
It is generated from a cryptographic source, prefixed so that a secret found in a log is identifiable as one, and never rendered again. An interface that can redisplay a secret is backed by a store holding something it should not.
Rotation overlaps deliberately
During a rotation two secrets are valid at once and deliveries carry a signature for each. That is why the header must be read as a list — a verifier that takes the first v1 value works until the day somebody rotates.
Retries back off
A delivery that fails is retried with exponential backoff. Return a 2xx as soon as you have durably accepted the body, and do the work afterwards — a receiver that processes before responding will be retried while it is still working.
One endpoint being down does not affect another
Deliveries are per-endpoint. A failing receiver accumulates its own retries rather than holding up anybody else’s events.
The endpoint must be reachable on the public internet
Private and loopback addresses are rejected when an endpoint is saved, rather than accepted and then failing silently at the first delivery.
The REST API: what is already decided
These decisions are settled, because each is expensive or impossible to change after the first integration depends on it. They are published now so that anything you design in the meantime can be designed against them. The endpoint list is not published, because it is not built.
Versioned in the path
Under /api/v1/. A version in a header is a version callers forget to send; a version in the path is visible in every log line and every bug report.
Bearer keys, issued per workspace
Sent as an Authorization header. Only a keyed fingerprint is stored, keys carry a visible prefix so a leaked one is identifiable without being usable, and revocation is read per request rather than cached — so it takes effect on the next call.
Cursor pagination, never offset
New opportunities arrive constantly and an offset page shifts underneath a caller walking it, silently skipping records. The failure is invisible until somebody reconciles counts.
One envelope
Every response carries data, meta and links. A list and a single record differ in what is inside data, not in the shape around it, and every response carries a request id.
Idempotent writes
Every mutating request accepts an idempotency key and replays the original response for a repeat within twenty-four hours. A response project created twice by a network retry is a real cost.
Rate limits you can read
Limit, remaining and reset on every response; a retry-after on a refusal. A per-minute limit and an exhausted monthly allowance are different answers, because the first is retryable and the second is a plan boundary.
Absence and denial look alike across a tenant boundary
A record belonging to another workspace returns a 404 rather than a 403, because a 403 confirms the record exists.
| Plan | API access | Monthly calls | Rate limit |
|---|---|---|---|
| Starter | Not included | — | — |
| Professional | Included | 25,000 | 60 per minute |
| Business | Included | 150,000 | 300 per minute |
| Agency | Included | 500,000 | 600 per minute |
These are the values held in the billing catalog, which is what the pricing page reads. They are the allowances that will apply when the surface opens; they are not enforceable against anything today, and no allowance accrues in the meantime.
While you wait
Integrations
CRM, document storage and single sign-on connections, configured in the workspace without a key.
Delivery and exports
A finished response leaves as a branded link or a PDF, which covers most of what an integration is asked to do with a document.
Tell us what you would call
Which resources, in which direction, and what your system is the record for. The read surface is specified; what lands first is not.
Plan allowances
Every limit, read from the billing catalog rather than restated — including the API allowances above.
Common questions
- Can I use the REST API today?
No. It is listed on three plans and the public surface is not built. The endpoints the web application calls are session-authenticated, shaped for its own screens, and will change without notice.
- Are webhooks available now?
Yes. An endpoint can be added in workspace settings, subscribed to notification categories, and every delivery is signed with the scheme documented on this page. Slack is available as a destination for the same events.
- Why does my signature check fail even though the secret is right?
Almost always because the body was parsed before it was verified. Re-serializing JSON changes key order, number formatting and unicode escapes, and the signature is computed over the bytes as sent. Capture the raw body first.
- What happens to deliveries during a secret rotation?
Two secrets are valid at once and each delivery carries a signature for both, so nothing is dropped while you change over. Read the header as a list of signatures rather than taking the first one.
- Why is API access sold on plans that cannot use it?
Because the entitlements, the metering and the allowances were built before the surface was. The choice was to stop listing it or to document it as forthcoming, and this page is that documentation.
- Will the decided parts of the API change before it ships?
The parts stated here — path versioning, bearer keys, cursor pagination, the envelope, idempotency and the tenant-boundary behaviour — are settled, because each is expensive to retrofit. The endpoint list is unpublished because it is unbuilt, not because it is secret.
- When will it be available?
There is no published date. An estimate on a page like this becomes a commitment somebody plans a quarter around, so if an integration is on your critical path, ask us directly and describe it — specified integrations are what order the work.
Related
CRM, storage, identity and the API everything else uses.
Four plans, read from the billing catalog rather than typed here.
How the product works, task by task.
How customer data is isolated, encrypted and retained.
See what you are not bidding on.
Connect a source, describe what your company does, and look at the opportunities that come back before deciding whether any of this is worth your time.