Bidrik API
Bidrik is a quoting and proposal platform for sales teams: create quotes from templates, send them to customers and get them accepted and e-signed online. The API gives other applications access to a Bidrik account's quotes, customers and templates. It is the API behind the Bidrik integration on Zapier.
Base URL: https://api.bidrik.com. Every request uses HTTPS and JSON. Questions: info@bidrik.com.
Authentication
The API uses OAuth 2.0 with the authorization code grant (RFC 6749). PKCE with S256 is supported (RFC 7636). Client credentials are issued by Bidrik; contact us to register an application.
Only account administrators can connect an application. The connection acts on behalf of the administrator's Bidrik account and sees all of its quotes and customers.
| Step | Endpoint | Notes |
|---|---|---|
| 1. Authorize | GET /oauth/authorize | Query: response_type=code, client_id, redirect_uri (must match a registered URI exactly), state, optional code_challenge + code_challenge_method=S256. The user signs in to Bidrik if needed and approves. Bidrik redirects to redirect_uri with code and state, or with error=access_denied. |
| 2. Exchange the code | POST /oauth/token | Form or JSON body: grant_type=authorization_code, code, redirect_uri, client_id, client_secret (or HTTP Basic), code_verifier if PKCE was used. Codes expire after 10 minutes and work once. |
| 3. Call the API | Authorization: Bearer <access_token> | Access tokens last 60 minutes. |
| 4. Refresh | POST /oauth/token | grant_type=refresh_token, refresh_token, client credentials. Returns a new access token and a new refresh token; the old refresh token stops working. A refresh token unused for 90 days expires. Reusing an old refresh token revokes the connection. |
| Revoke | POST /oauth/revoke | token (access or refresh) and client credentials (RFC 7009). Revokes the whole connection. Always answers 200. |
Token response:
{
"access_token": "bat_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "brt_…"
}
A connection stops working when the user is removed or deactivated, loses the administrator role, or the Bidrik account is closed. API calls then answer 401 and refreshing fails with invalid_grant; the user needs to reconnect.
Conventions
- Request bodies are JSON (
Content-Type: application/json). Responses are JSON. - Ids are 24-character hexadecimal strings.
- Timestamps are ISO 8601 in UTC. A value that is not set is
null. - Lists answer
{ "data": [ … ] }, newest first. Page withlimit(1–100, default 50) andpage(from 1). - Rate limit: 120 requests per minute per connection. Above it the API answers
429withRetry-After. - New fields may be added to responses at any time. Clients should ignore fields they do not know.
Errors
Errors answer a non-2xx status and this body:
{ "error": { "code": "duplicate_customer", "message": "A customer with this name already exists. …" } }
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter is missing or malformed; error.field names it. |
| 400 | invalid_json | The body is not a JSON object. |
| 401 | missing_token, invalid_token, user_inactive, account_inactive, admin_required, not_account_member | The access token cannot be used. Refresh it; if refreshing fails, reconnect. |
| 402 | trial_limit_reached | The account is on a trial and has used its documents. Carries used and limit. |
| 404 | not_found, template_not_found, customer_not_found | No such object in the connected account. |
| 405 | method_not_allowed | Wrong HTTP method; see Allow. |
| 409 | duplicate_customer | Another customer has the same name. Send allow_duplicate: true to save anyway. |
| 409 | fortnox_sync_active | The account syncs customers with Fortnox; customers cannot be written through the API. |
| 422 | validation_failed | Bidrik rejected the object; error.fields maps field to reason. |
| 429 | rate_limited | Too many requests. |
| 500 | internal_error | Our fault. error.reference helps us find it. |
Account
GET /v1/me
The connected user and account. Used to test a connection.
{
"id": "66e0c2f1a9b4c3d2e1f00c33",
"email": "anna@example.com",
"name": "Anna Andersson",
"account": { "id": "66e0c2f1a9b4c3d2e1f00d44", "name": "Example Sales AB" },
"plan": "paid",
"documents": { "used": 212, "limit": null }
}
plan is paid or trial. On a trial, documents.limit is the number of documents (quotes, templates, product sheets and agreements) the account may have.
Quotes
GET /v1/quotes
Quotes in the account, newest first.
| Query | Description |
|---|---|
sort | created (default) or updated. Event orders: sent, opened, accepted, rejected — most recent event first, and only quotes where that event has happened, whatever their status is now. expires orders by expiry date, latest first. |
status | Only quotes with this current status: draft, sent, opened, accepted, rejected, delivered, followup, expired. |
expired | true: only sent quotes whose expiry date is before today (UTC) and that are not accepted, rejected or delivered. A quote valid until a date stays valid all of that day. Cannot be combined with status. false: only quotes that have not expired. |
customer_id | Only quotes for this customer. |
limit, page | Paging. |
Response: { "data": [ quote, … ] }. A quote:
{
"id": "66e0c2f1a9b4c3d2e1f00a11",
"number": "1042",
"title": "Office renovation",
"status": "accepted",
"value": 125000,
"currency": "SEK",
"customer_id": "66e0c2f1a9b4c3d2e1f00b22",
"customer_name": "Example AB",
"owner_name": "Anna Andersson",
"owner_email": "anna@example.com",
"created_at": "2026-09-01T08:12:00.000Z",
"updated_at": "2026-09-03T14:40:12.000Z",
"valid_from": "2026-09-01T00:00:00.000Z",
"expires_at": "2026-10-01T00:00:00.000Z",
"sent_at": "2026-09-01T09:00:00.000Z",
"opened_at": "2026-09-02T07:31:00.000Z",
"accepted_at": "2026-09-03T14:40:12.000Z",
"rejected_at": null
}
| Field | Description |
|---|---|
number | The quote number shown to the customer. |
status | accepted means the customer accepted or signed the quote. |
value | Total value as a number, in currency. |
owner_* | The Bidrik user responsible for the quote. |
GET /v1/quotes/{id}
One quote. 404 not_found if it does not exist in the account.
POST /v1/quotes
Creates a draft quote from a template, for a customer. The quote gets the next quote number and the connected user as owner, exactly as if it was created in Bidrik. It is not sent.
| Body field | Description | |
|---|---|---|
template_id | required | Template to create the quote from. See GET /v1/templates. |
customer_id | required | Customer the quote is for. |
title | optional | Title of the quote. Defaults to the template's name. |
POST /v1/quotes
{ "template_id": "66e0c2f1a9b4c3d2e1f00e55", "customer_id": "66e0c2f1a9b4c3d2e1f00b22", "title": "Office renovation" }
Answers 201 with the quote. 402 trial_limit_reached when a trial account has used its documents.
Customers
GET /v1/customers
Customers in the account, most recently created first. Response: { "data": [ customer, … ] }. A customer:
{
"id": "66e0c2f1a9b4c3d2e1f00b22",
"name": "Example AB",
"is_private": false,
"org_number": "556677-8899",
"customer_number": "1001",
"email": "hello@example.com",
"website": "https://example.com",
"vat_number": "SE556677889901",
"currency": "SEK",
"contact": {
"first_name": "Erik",
"last_name": "Svensson",
"email": "erik@example.com",
"phone": "+46 70 123 45 67",
"title": "CEO"
},
"address": {
"street": "Storgatan 1",
"postal_code": "111 22",
"city": "Stockholm",
"country": "Sweden"
},
"owner_name": "Anna Andersson",
"owner_email": "anna@example.com",
"created_at": "2026-08-20T10:00:00.000Z"
}
A private customer (is_private: true) is a person; its name is the contact's name.
GET /v1/customers/search
Finds customers by exact match, ignoring case. Give at least one parameter; several must all match. Returns at most 10, newest first.
| Query | Matches |
|---|---|
email | The customer's email or the contact's email. |
name | The customer's name. |
org_number | Organisation number, ignoring dashes and spaces. |
customer_number | The customer number. |
Response: { "data": [ customer, … ] }, empty when nothing matches.
GET /v1/customers/{id}
One customer. 404 not_found if it does not exist in the account.
POST /v1/customers
Creates a customer. Its owner is the connected user unless owner_email names another user. Answers 201 with the customer.
| Body field | Description |
|---|---|
name | Company name. Required unless is_private. |
is_private | true for a private person. Then contact.first_name is required. |
org_number, customer_number, email, website, vat_number | Text. |
currency | Three-letter code, e.g. SEK. |
contact | Object: first_name, last_name, email, phone, title. |
address | Object: street, postal_code, city, country. |
owner_email | Email of the Bidrik user responsible for the customer. Must be an active user in the account, otherwise 400 invalid_parameter and nothing is saved. |
allow_duplicate | true to save even if another customer has the same name. Otherwise 409 duplicate_customer. |
POST /v1/customers
{
"name": "Example AB",
"org_number": "556677-8899",
"email": "hello@example.com",
"contact": { "first_name": "Erik", "last_name": "Svensson", "email": "erik@example.com" },
"address": { "city": "Stockholm", "country": "Sweden" }
}
Accounts that sync customers with Fortnox answer 409 fortnox_sync_active.
PATCH /v1/customers/{id}
Updates the fields you send; everything else is kept. Same fields as POST /v1/customers. An empty string leaves a field unchanged; send null to clear it. Answers 200 with the customer.
PATCH /v1/customers/66e0c2f1a9b4c3d2e1f00b22
{ "email": "billing@example.com", "contact": { "phone": "+46 70 765 43 21" } }Templates
GET /v1/templates
The account's quote templates, by name. Used to pick a template for POST /v1/quotes.
{ "data": [ { "id": "66e0c2f1a9b4c3d2e1f00e55", "name": "Standard quote", "updated_at": "2026-06-11T12:00:00.000Z" } ] }Version 1. Breaking changes get a new version prefix; additions do not.